Заменяй и властвуй — подход SOLID для разработки повторно используемых компонентов в вебе

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


    Эта статья во многом вдохновлена докладом Павла Силина на РИТ 2017, однако здесь много моего собственного опыта и размышлений. Примеры будут на React + TypeScript, однако подход не привязан к какой-либо технологии.




    Как не надо делать


    Когда встречаешься с ситуацией дублирования кода, естественным желанием становится вынести этот код в отдельный компонент и использовать везде, где нужно. Возьмем для примера модальное окно. Казалось бы, что может быть проще — взяли и сделали:


    ShowModalWindow(header: string, content: JSX.Element): Promise<ModalWindowResult>;

    Все отлично, дублирование кода устранено, мы счастливы. Но вот мы продолжаем разработку, и в каком-то случае оказалось недостаточно одной кнопки "ОК", нужна еще и "Отмена". Укоряем себя, что сразу не подумали, и добавляем параметр:


    ShowModalWindow(header: string, content: JSX.Element, buttons?: string[]): Promise<ModalWindowResult>;

    Проблема решилась, разработка идет дальше. В один прекрасный момент тестировщики находят багу — если открыть два модальных окна подряд, то затемнение фона накладывается и становится слишком темным. ОК, тут уже трудно себя укорить — разве можно было это предусмотреть? Ну да ладно, добавляем еще параметр:


    ShowModalWindow(header: string, content: JSX.Element, buttons?: string[], showOverlay?: boolean): Promise<ModalWindowResult>;

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


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


    В моем примере была функция, но это может быть что угодно — реакт-компонент с огромными props, jquery-плагин со множеством опций, базовый класс с кучей наследников и переопределяемых методов, ASP.NET Rasor хелпер, со множеством параметров, scss mixin и т.д. Наступить на эти грабли можно в любой технологии и в самых разных видах.


    Заменяй и властвуй


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


    Однако важно соблюдать их все сразу, а не по отдельности. Допустим, недостаточно просто делать "маленькие компоненты". Это только первая буква S, и без всех остальных это ничего не даст. Давайте пробежимся по всем буквам:


    1. S (single responsibility) — делать повторно используемые компоненты очень маленькими и простыми, с минимумом ответственности;
    2. O (open-closed) — никогда, ни при каких обстоятельствах не модифицируем код компонентов, которые часто используются;
    3. L (Liskov substitution) — любой компонент может быть заменен другим так, что все остальные компоненты не должны заметить подмены;
    4. I (interface segregation) — вместо написания "обобщенных" компонентов на все случаи жизни пишем простые конкретные реализации;
    5. D (dependency inversion) — решение о том, какой из компонентов будет использован в каждом случае, должен принимать вызывающий код.

    На практике это выглядит следующим образом. Мы пишем простые (до безобразия простые) компоненты, которые сочленяются между собой как детальки LEGO. Ни одна деталька ничего не знает о других. Когда нужно сделать конкретную вещь — мы берем эту коробку конструктора и составляем именно то, что нам нужно. Если какая-то деталь нам не подходит, мы запросто можем ее выкинуть, и взять другую (например, сделать свою). Это очень просто, потому что каждая из деталей сама по себе тривиальная, и ничего не стоит сделать другую, похожую, но подходящую под данный конкретный случай. Так что, вместо того чтобы изменять существующие компоненты, мы просто заменяем их, благодаря чему мы не можем даже теоретически что-то сломать в другом месте приложения.


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


    Давайте отрефакторим наше модальное окно в соответствии с этими принципами. Нам нужно сделать модальное окно примерно следующего вида:



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


    1. Позиционирование окна — располагает что-либо по центру экрана;
    2. Затемнение фона — создает полупрозрачный div на весь экран;
    3. Коробка окна — определяет размеры и заливку внутри окна;
    4. Коробка заголовка — добавляет отступы для заголовка и рисует разделительную линию;
    5. Заголовок — опередяет стилизацию текста заголовка (в основном размер шрифта);
    6. Кнопка закрытия (крестик);
    7. Коробка содержимого — добавляет отступы для содержимого окна;
    8. Коробка кнопок диалога — добавляет отступы и позиционирует кнопки в правую часть;
    9. Кнопка — просто обычная кнопка, никак не связанная с диалогом.

    Получается что-то вроде этого:


         <ModalBackdrop onClick={() => this.setState({ dialogOpen: false })} />
             <ModalDialog open={this.state.dialogOpen} >
                 <ModalDialogBox>
                     <ModalDialogHeaderBox>
                         <ModalDialogCloseButton onClick={() => this.setState({ dialogOpen: false })} />
                         <ModalDialogHeader>Dialog header</ModalDialogHeader>
                     </ModalDialogHeaderBox>
                     <ModalDialogContent>Some content</ModalDialogContent>
                     <ModalDialogButtonPanel>
                         <Button onClick={() => this.setState({ dialogOpen: false })} key="cancel">
                             {resources.Navigator_ButtonClose}
                         </Button>
                         <Button disabled={!this.state.directoryDialogSelectedValue}
                             onClick={this.onDirectoryDialogSelectButtonClick} key="ok">
                             {resources.Navigator_ButtonSelect}
                         </Button>
                     </ModalDialogButtonPanel>
                 </ModalDialogBox>
             </ModalDialog>
         </ModalBackdrop>

    Каждый из этих компонентов, как правило, добавляет один div и несколько css-правил. Например, ModalDialogContent выглядит так:


        // JS
        export const ModalDialogContent = (props: IModalDialogContentProps) => {
            return (
                <div className="modal-dialog-content-helper">{props.children}</div>
            );
        }
        // CSS
        .modal-dialog-content-helper {
            padding: 0 15px 20px 15px;
        }

    Если в будущем мне понадобится сделать модальное окно с другими отступами, то я просто заменю ModalDialogContent на обычный div, и задам свои собственные отступы. Если мне понадобится убрать затемнение, я просто уберу ModalBackdrop. Такая гибкость достигается за счет соблюдения всех принципов SOLID: компоненты простые и конкретные (S, I), ничего друг о друге не знают (D), поэтому проще их заменить (L), чем добавлять какие-то опции (O).


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


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


       <CommonModalDialog header="Header text" 
              isOpen={this.state.open} onClose={this.onClose}>
           Modal content
       </CommonModalDialog>

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


    Назад в реальность


    В идеальном мире мы бы везде писали все по SOLID, ходили в белых одеждах и хлопали от счастья крылышками. К сожалению, в реальности этот подход применять не везде оправдано, а местами даже вредно. Вот его основные недостатки:


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

    Рассмотрим подробнее на примере меню пользователя вконтакте.



    Можно начать разбивать его на кучу независимых маленьких компонентов, отдельно будет иконка пользователя, отдельно имя, отдельно менюшка… Мы потратим кучу сил на то, чтобы организовать взаимодействие между этими независимыми компонентами. Но что в итоге мы получим? Это меню существует в единственном числе, и только в таком виде. У этого меню есть некоторая логика — своя единая модель данных (информация о пользователе), определенный состав меню (набор действий), поведение (по щелчку открывается меню). Все это логика конкретного приложения, которая определяется бизнес-задачами и диктуется предметной областью. Размазывая эту логику по многим местам, не только создаем себе лишние трудности, но и усложняем поддержку и сопровождение нашего сайта. Другому программисту будет трудно найти место, где вешается обработчик на событие клика, который открывает менюшку, потому что он будет (безусловно по SOLID) запрятан где-нибудь в глубинах нашей архитектуры.


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


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


    Заключение


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


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

    ДоксВижн 35,58
    Компания
    Поделиться публикацией
    Комментарии 166
    • –1
      Что-то полный бред. Клик, это бизнес логика, которая диктуется предметной областью?
      Такое ощущение, что кто-то только вчера открыл для себя новые слова, но ещё не успел постичь их смысл…
      • 0

        Вы о чем конкретно? Опишите подробнее свою мысль.

        • +1

          В общем случае, разработка меню или модального окна для конкретного сайта/веб-приложения не имеет никакого отношения к бизнес-логике, это чистая логика представления, логика UI. Бизнес-логикой она становится, если ваша задача разработать UI-фреймворк.

          • 0

            Все правильно, модальное окно это повторно-используемый компонент, который не должен быть связан с бизнес-логикой текущего приложения. Его можно оформить в виде отдельного npm-пакета, как это, например, делают эти парни или использовать готовый. Я где-то написал, что модальное окно это бизнес-логика?

            • +1
              У этого меню есть некоторая логика — своя единая модель данных (информация о пользователе), определенный состав меню (набор действий), поведение (по щелчку открывается меню). Все это бизнес-логика, которая определяется бизнес-задачами и диктуется предметной областью.

              А это что?

              Описание Вашего «уникального» подхода к архитектуре является иллюстрацией классического не понимания, как ооп, так и всех принципов, которые Вы так тщательно пытались тут описать.

              У КОМПОНЕНТА ModalDialogBox две ответственности, размеры и цвет… предметная область… Вы о чем? Вы даже не представляете какую безбожную ерунду Вы написали.

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

              На каждый пчих своя коробка… какая коробка, Вы грузчик? Разве это переиспользование?

              Если судить по себе, то складывается впечатление, что автор занимается программированием не больше полугода. И как венец архитектурного гения и накопленного личного опыта, та-дам! buttons?: string[]
              • 0

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


                Отсюда следует, что нужно четко разделять повторно используемые компоненты и компоненты бизнес-логики.
                • 0

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

                  • –2
                    Возможно это покажется бестактно, но Ваше изложение показало что Вы вообще не понимаете что такое бизнес логика и предметная область. Вот если бы я сказал, что автомобиль можно скинуть с 10 метровой высоты и ему ничего не будет, основываясь на характеристиках ризины используемой при изготовлении шин Лунохода, то Вам бы показалось что я неверно сформулировал мысль?

                    И Вы пытаетесь случаем не пытаетесь улизнуть от ответа на непонимание основ ооп и dry + solid, о которых собственно и статья? Вы понимаете что статья не просто неверна, она ещё и вред принесет и таких как Вы будет еще больше, а работать с ними потом мне? Прежде чем учить, выучитесь сами. пол года программирования css + html не дает право писать такую ень.

                    А если в подобных ситуациях соблюдать такт, то это сделает только хуже всем.
                    • 0

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

                      • 0
                        Можно откровенный вопрос — Вы вообще программист? Да, у всех бывают ошибки, в том числе и у меня, и именно в такие моменты, я проявляю толерантность и либо пытаюсь донести свои мысли, либо молчу. Но в подобных ситуациях, я всегда говорю что это полный бред и что я даже комментировать не хочу. Мне безразлично мнение тех, кто посмотрит на мой рейтинг, меня больше заботят те, кто будет этот треш читать. И меня ещё раз простите, могу ошибаться, но у Вас в подписи есть указания что Вы каким-то образом причастны к вэбинарам…? Вы вот такую фигню в мозг другим людям вдалбливаете? ну это же просто вредительство в массовом масштабе. Компоненты бизнес логики, а нет Компоненты приложения. Это вообще безумие.
                        • +1
                          Судя по треду, дело в вашем недопонимании.

                          Автор в целом пишет правильные вещи, но местами могут быть проблемы с терминологией, – это нормально. Тут главное не терминология, а сама идея.

                          То как будет отображаться меню – это UX, который выстраивается из требований бизнеса (т.е. своего рода бизнес-логика для отображения).
                          Вы же не делаете менюшки такими как вам захочется и не лепите их куда вам захочется?

                          А компоненты и правда делятся на: компоненты общего назначения (их можно переиспользовать) и компоненты, которые выполняют конкретную поставленную задачу (бизнес требование).
                          • 0
                            А компоненты и правда делятся на: компоненты общего назначения (их можно переиспользовать) и компоненты, которые выполняют конкретную поставленную задачу (бизнес требование).

                            Не правда. "конкретные" компоненты тоже можно и даже нужно переиспользовать. Незачем каждый раз при необходимости ввести данные паспорта велосипедить этот процесс по новой.

                            • 0

                              Скажем так, разница в необходимой гибкости. Если компонент всегда выглядит одинаково, то для него SOLID не обязателен. Если в каждом случае его использования может потребоваться что-то адаптировать, то есть смысл подумать о SOLID.


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

                              • +1

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

                                • 0

                                  SOLID подход не простой, поэтому использовать его везде — дорого. Вы можете предложить что-то такое же стабильное но более простое? В примере с формой в другой ветке я не увидел у Вас ничего нового — просто компоненты передаются через пропсы. Или я чего-то не понял?

                                  • 0

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

                              • 0
                                компоненты общего назначения – можно переиспользовать и в других проектах без изменения.

                                «конкретные» компоненты – можно/нужно переиспользовать в пределах текущего проекта.

                                на счет последних: если компонент нужно «серьезно» допиливать, чтобы переиспользовать в проекте, то в этом случае лучше создать новый.
                                • 0
                                  «конкретные» компоненты – можно/нужно переиспользовать в пределах текущего проекта.

                                  Их можно/нужно использовать там, где они повторяются © Ваш Кэп


                                  Типичный пример — процесс авторизации (а это 1..10 экранов) должен выглядеть и функционировать единообразно для всех приложений компании и подключаться одной строчкой "для доступа к этому разделу требуется авторизация".

          • –3
            Если кто-то не понимает того что написано в этой статье, то мой Вам совет, забудьте все слова которые тут написаны, кроме dry и solid или не читайте вовсе.

            Вы о чем конкретно? Опишите подробнее свою мысль.

            К сожалению не там ответил. Все неправильно, все наоборот и все полный бред. Пустьменя заминусуют, но я не хочу тратить все время на аргументацию полного бреда.
            • 0
              Неизбежно разрастающееся количество опций у компонента — это «попахивает», но с этим еще как-то можно мириться.

              Для решения данной проблемы обычно используется паттерн «фабрика». Однако он может входить в противоречие с принципом Interface segregation.
              • 0
                Эта статья во многом вдохновлена докладом Павла Силина на РИТ 2017

                Бегло глянув на доклад, часть претензий по непониманию принципов SOLID и вообще ООП можно предъявить его автору. Основной посыл докладчика — в javascript интерфейсов нет, наследование есть, однако использовать его он не рекомендует (дословно цитируя — «моя практика показала, что использование наследования в React-компонентах это приводит к больше проблемам, чем к каким-то профитам»), но про SOLID он читал, и поэтому будет пытаться как-то связать прочитанное с темой доклада. Ведущий front-end разработчик…
                • 0

                  Я выше уже писал, это статья не о SOLID. Я, как и Павел, просто используем идеи из SOLID. Или Вы считаете, что если у меня функция вместо класса, то принципы SOLID никак нельзя применить?

                  • 0
                    Если из SOLID выбросить 2 или 3 принципа (за «ненужностью» или из-за того, что в языке javascript чего-то нет), это будет уже не SOLID. Зачем тогда вообще упоминать эти 5 принципов?

                    Не скажу, что я гуру в терминологии, но расшифровка SOLID и в докладе и в тексте Вашей статьи, плохо соответствует тому, как лично я их понимаю.

                    S — компоненты (классы) не обязательно должны быть маленькими и простыми, но должны иметь узкую специализацию. Условно, если есть класс «персона» (или как правильнее перевести то, что в английском вкладывается в слово person), у него может быть хоть 10000 методов и миллион строк кода, но если они связаны исключительно со свойствами абстрактной персоны, то принцип single responsibility де-юре не нарушается.

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

                    L — компонент может быть заменён не просто на другой, а на такой, который реализует те же самые методы с той же самой логикой/поведением (но помимо этого может реализовывать ещё какие-то новые методы, которых не было в старом компоненте).

                    I — если компонент может, в зависимости от входных параметров, быть слишком разным (будь то внешний вид или поведение), то предпочтительнее выделить эти «разные» версии в виде отдельных классов, с более мелкими компонентами легче работать, чем помнить все варианты зависимости поведения «жирного» компонента от входных параметров.

                    D — не являюсь специалистом по javascript и front-end-у, но dependency injection в javascript якобы возможно, например, об этом писалось в habrahabr.ru/post/232851
                    • 0

                      Спасибо, можете еще добавить, что из этого не соответствует представленному в статье описанию? Не обязательно маленькие, здесь я согласен. Но в случае компонентов в веб они чаще всего маленькие. Если они большие, то скорее всего там слишком много ответственности. В букве О я делаю чуть более жесткие требования — не рекомендую вообще менять, даже рефакторить. D — разве суть DI не в том, чтобы зависимости резолвились из вне? Или все что не Composition Root это не DI?

                      • +1
                        Я повторюсь, что single responsibility несёт в себе несколько иной смысл, чем просто размер компонента. Пусть у нас будет даже маленький компонент, даже крохотный, у которого будет только одно свойство, допустим строка value, и пара методов — get/set. То, как оно будет визуализироваться в html — это уже должен быть другой компонент. Нельзя будет добавить в первый компонент метод showAsHtml, не нарушив при этом принцип S.

                        Принцип open/closed, условно, пусть у меня будет в коде цикл for(i=0; i <= myArray.length — 1; i++), почему я не могу поменять его на for(i=0; i < myArray.length; i++)? Что это нарушит в логике/поведении программы? Ничего. Если быть осторожным (и правильно покрыть код unit-тестами), то рефакторинг вполне допустим и не нарушает принцип O.

                        Принципы L и D я в предложенном решении не вижу.

                        Принцип interface segregation, возможно, соблюдён для мелких «деталек LEGO», но в самом решении конструирования диалогового окна из кубиков принцип I вообще не соблюдается. Это несоблюдение, само по себе. не хорошо и не плохо, даже самые полезные принципы и паттерны могут не подходить для решения каких-то конкретных, специфичных проблем, и в этом нет ничего зазорного.

                        Как я уже писал выше, конструирование диалогового окна из деталек я бы вынес в отдельный factory-класс, в котором был бы набор facade-методов вроде dialogWithTextAndOkButton(text, okButtonCallback), dialogWithTextAndOkButtonAndXClose(text, okButtonCallback, closeCallback) или что-то подобное.
                        • 0
                          Что это нарушит в логике/поведении программы?

                          Дело в том, что в web даже если ты немножко меняешь верстку, то это может сильно повлиять на тех, кто использует. Кроме того, верстку очень трудно покрывать unit-тестами (почти невозможно). Поэтому я ужесточаю правило.


                          single responsibility несёт в себе несколько иной смысл

                          Полностью с Вами согласен, но все-же статью править не буду. Именно из-за специфики веба, размер здесь особенно важен.


                          я бы вынес в отдельный factory-класс

                          Да, можно делать хелперы, которые принимают набор параметров, и собирают из них LEGO нужной конфигурации. Я не стал это описывать, мне кажется это очевидным. Но использовать эти хелперы не обязательно, всегда можно заюзать компоненты напрямую. В этом смысл всего подхода.


                          но в самом решении конструирования диалогового окна из кубиков принцип I вообще не соблюдается.

                          Поясните, не пойму эту мысль. Когда мы конструируем окно, то речь уже идет о "компонентах приложения". Как я описываю в последней части статьи, здесь уже не применяется SOLID, там уже играют роль другие соображения — целостность логики и т.д.


                          Принципы L и D я в предложенном решении не вижу

                          L: Вы же сами пишите — "компонент может быть заменен на любой другой" — именно это и фигурирует в названии статьи "заменяй и властвуй". D: вызывающий код выбирает, из каких компонентов он собирает окно. Вызывающий код является внешним для компонентов LEGO, то есть зависимости резолвятся из вне. Что это если не DI?

                          • +1
                            Изменение вёрстки — это изменение поведения, я изначально сказал, что рефакторинг его не должен менять.

                            Слишком общее решение может быть не удобно для использования и может увеличивать количество ошибок. Легче один раз написать и отладить helper (в общепринятой терминологии facade), чем помнить как, с какими параметрами и в какой последовательности нужно выполнить 5-10 простых, казалось бы, операций. То есть супер-гибкий метод может быть доступен, но, скорее всего, в 90% случаев будет достаточно пяти вариантов диалогового окна, и лучше иметь эти 5 вариантов в виде пяти простых и отлаженных методов.

                            Я не стал бы переводить interface segregation как разделение интерфейса, это скорее изоляция вариантов использования интерфейса, разделение его на несколько частей с более узкой специализацией. Условно, вот есть у Вас это гибко-настраиваемое диалоговое окно. И есть в приложении 10 вызовов этого окна, где нужен лишь заголовок, input-поле и кнопка «ок». А также один единственный вызов этого диалогового окна, где, допустим, будет заголовок, под ним строка для поиска с кнопкой «искать», внизу слева древовидный справочник для выбора значения, справа при click-е на значении в дереве будет показываться какой-то текст, и внизу кнопка «ок». Принцип interface segregation советует разделить эти два варианта на разные классы/компоненты, так как по отдельности ими будет легче пользоваться.

                            Дело не в «из каких компонентов он собирает окно», а в том, что DI-код сборки, условно, встретив Some content, может динамически (то есть в runtime) выбрать один из нескольких вариантов отрисовки этого компонента, причём он сам решает исходя из каких-то параметров или контекста, какой именно вариант выбрать. А может быть и только один вариант такого компонента, или вообще не одного, мало ли при сборке проекта что-то потерялось.
                            • 0
                              Прошу прощения, <ModalDialogContent>Some content</ModalDialogContent> заменилось на просто Some content.
                              • 0
                                Изменение вёрстки — это изменение поведения

                                Компоненты, о которых я говорю, в основном только версткой и занимаются. Бывают компоненты с логикой, там рефакторинг и unit-тесты возможны, да.


                                Легче один раз написать и отладить helper (в общепринятой терминологии facade), чем помнить как, с какими параметрами и в какой последовательности нужно выполнить 5-10 простых, казалось бы, операций.

                                Разумно, соглашусь.


                                Принцип interface segregation советует разделить эти два варианта на разные классы/компоненты, так как по отдельности ими будет легче пользоваться.

                                Вроде именно об этом я и пишу. Подскажите, в чем отличие?


                                может динамически (то есть в runtime) выбрать один из нескольких вариантов

                                Когда резолвить зависимости — runtime или compile-time это уже не так принципиально, и зависит от потребностей в каждом случае. Среди задач которые мне встречались, динамическое сопоставление зависимостей привнесло бы только лишнюю сложность и не дало никакой выгоды. Можно, конечно, для каждого LEGO придумать фабрику и т.д., но это уже будет over-проектирование. Для фронтенда я такого не встречал.

                                • 0
                                  Сложно объяснить это на примере javascript. Условно, в Java у меня вместо относительно (пример искусственный, на самом деле методов может быть не 4, а, скажем, 100) переусложнённого варианта

                                  interface ModalDialogWindow {
                                  void setupInputFieldVersion();
                                  void showInputFieldVersion();
                                  void setupTreeDirectoryVersion();
                                  void showTreeDirectoryVersion();
                                  }

                                  был бы
                                  interface ModalDialogWindow {
                                  void show();
                                  }
                                  interface InputFieldModalDialogWindow extends ModalDialogWindow {
                                  void setupInputField();
                                  void show();
                                  }
                                  interface TreeDirectoryModalDialogWindow extends ModalDialogWindow {
                                  void setupTreeDirectory();
                                  void show();
                                  }
                                  то есть я разделил бы общий случай на более специализированные.

                                  Про DI также сложно объяснять пишущим на javascript. На Java у меня будет, допустим,

                                  interface A {}
                                  class B implements A {}
                                  class C implements A {}

                                  В коде у меня будет заведено поле типа A (экземпляр interface или abstract class создать невозможно, можно создавать только экземпляры не-абстрактных class-ов), и мне кто-то (DI-контейнер) подставит в это поле экземпляр либо типа B либо типа C, он сам по какой-то (известной, не случайной) логике решит, какой именно, B или C подставлять, не я в коде прямо впишу создание экземпляра конкретного класса

                                  A a = new B();

                                  или по какому-то условию буду создавать экземпляр

                                  A a;
                                  if(someVar == 'B') { a = new B(); }
                                  else if(someVar == 'C') {a = new C(); }

                                  а DI-контейнер за меня это сделает.
                                  Затем я, допустим, добавлю

                                  class D implements A {}

                                  мне ну нужно будет переписывать код, добавляя в него ещё одно условие

                                  else if(someVar == 'D') { a = new D(); }

                                  а неким иным способом смогу присваивать переменной 'a' экземпляр типа 'D', без изменения кода.
                                  • 0

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


                                    Про DI, я понимаю о чем Вы, я даже делал свою либу для TypeScript. Seeman пишет, что лучший DI это через конструктор. Он еще не очень любит DI-контейнеры, и в CompositionRoot предпочитает кидать зависимости в конструктор вручную (Poor Man's DI). В этом случае весь DI сводится к тому, что вы создаете объекты и передаете их параметрами в конструктор другим объектам.


                                    В случае с React все немного иначе. Во-первых, мы не можем использовать конструктор, реакт сам создает объекты. Вместо этого, мы передаем зависимости либо через аттрибуты, либо через children. Последнее выглядит так:


                                    <ModalDialog>
                                         <ModalDialogContent></ModalDialogContent>
                                    </ModalDialog>

                                    Здесь ModalDialogContent передается как children для ModalDialog. ModalDialog при этом задает требования к children — они должны быть реакт-компонентами. То есть, реализовывать соответствующий интерфейс. Он может, конечно, потребовать более конкретный интерфейс, но часто этого не нужно.


                                    К сожалению, настоящий CompositionRoot здесь затруднительно сделать, и вообще не нужно. Вместо этого, какждый компонент приложения выступает в качестве мини-composition-root, и собирает, допустим, модальное окно для своих нужд. Для фронтенда такой гибкости в управлении зависимости обычно достаточно.

                                    • 0
                                      Разделение программного interface на части это нечто совсем иное, чем декомпозиция визуального интерфейса на компоненты.

                                      Как именно сделано DI это менее важная вещь, чем то, что в поле или в параметр у нас могут попадать interface либо «родительский» class, у которого могут быть несколько «детей», имеющих «докрученную», более специализированную логику. Возвращаясь к примеру, у меня будет тип ModalDialogWindow с методом show(), а уж какое конкретное окно будет создано, InputField или TreeDirectory это будет разрулено в runtime контейнером DI.

                                      Я не могу сказать, насколько DI важно или реализуемо в front-end разработке, так как больше специализируюсь на back-end и БД. Всё, что я хотел сказать — то, что описано в статье — не содержит DI.
                        • +1
                          D — не являюсь специалистом по javascript и front-end-у, но dependency injection...
                          Классическая ошибка.
                          D из SOLID – это Dependency Inversion, а не Dependency Injection.

                          Dependency Injection говорит, что должен существовать некий DI контейнер, в который будет производить инстанциирование объектов и внедрение зависимостей (это уже развитие идеи и конкретная имплементация принципа D из SOLID).

                          Dependency Inversion гласит лишь о том, что зависимость будет передана из вне. Точка.
                          Это может быть передача параметром в конструктор / сеттер, а может быть билдер, а может быть фабрика, это может быть DI контейнер, ну или на крайний случай ServiceLocator.

                          Применимо к статье: Создается компонент и ему через проперти передается конкретная реализация (инстанс) компонента, который должен будет отобразиться; или функция отрисовки компонента (provider).

                          <MyComponent header={this._renderHeader()} body={this._renderBodyProvider} />
                          • 0
                            DI-контейнер это типичный подход к реализации принципа, однако нигде не говорится, что injection реализуется исключительно DI-контейнером. Любой внешний код, создающий экземпляр объекта и передающий его в другой объект (через конструктор, setter или присваиванием значения полю) является injector-ом, то есть реализует dependency injection.
                            • 0
                              Перечитал оригинал, мы оба не правы. Там совсем про другое написано, условно про три уровня:

                              class MyApp {
                              void doSomething(A a) {
                              a.someMethod();
                              }
                              }

                              interface A {
                              someMethod();
                              }

                              class AImpl implements A {
                              someMethod() {}
                              }

                              На том, как именно будет создан объект 'a', который будет передан в метод doSomething класса MyApp, в статье внимание не заостряется.
                            • 0
                              Прежде чем предъявлять кому либо претензии по непониманию принципов SOLID и ООП, стоит самому их изучить.
                              1) если ваш класс будет иметь 10000 методов или миллион строк, он 100% нарушит SRP. Небольшой размер компонента или класса это следствие от соблюдения SRP и Interface segregation.
                              2) Добавление методов с новой логикой — обязательно нарушит Open Closed, рефакторинг также нарушает данный принцип. Нужны новые методы — сделай наследника, нужен рефакторинг — создай новый класс, нужна расширяемость — сделай api позволяющее расширять или менять поведение. Написанный и протестированный класс больше ни когда не должен трогаться.
                              3) Принцип Лискова, как раз про то что те кто работаю с классом A должны корректно работать и с классом B (наследником от A) и логика класса B может отличаться от класса A и у вас явное не понимание данного принципа.
                              4) Interface Segregation совсем не про разные версии и поведение одного компонента, а про соответствие одновременно нескольким разным интерфейсам. Не стоит делать class A implements Foo, Bar, Baz {}, следствием данного принципа будет как раз раздутое и плохое апи класса или компонента.
                              5) Dependency Inversion != Dependency injection, принцип совсем не про dependency injection и он как раз сильно завязан на интерфейсах, в javascript нету типизации и интерфейсов и dependency injection не поможет соблюдать dependency inversion, как в типизированных языках.

                              И да статья не про SOLID, как и мой доклад. А о принципах по мотивам SOLID.
                              • +1
                                1) Нигде в оригинале не говорится о размере или количестве методов. Если у класса есть 10000 свойств (полей) и getter-ы/setter-ы либо вычисления для всех этих 10000 свойств, и больше никаких других методов (скажем, вывод этих свойств в файл или показ на экране), то принцип SR не нарушен.

                                2) Добавление новых методов не нарушает OC. В оригинале, помимо прочего, говорится:
                                «It should be clear that no significant program can be 100% closed.… Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design.»
                                В статье про L упоминается «The primary mechanisms behind the Open-Closed principle are abstraction and polymorphism», в случае если у меня класс implements SomeInterface, добавление в него новых методов или изменение старых методов, которые не перечислены в SomeInterface, не нарушает OC.

                                Рефакторинг существующего кода — возможно является нарушением де-юре, однако если этот рефакторинг никоим образом не меняет логику/поведение (на чём я заострил внимание), то де-факто он не привносит проблем, с которыми борется OC («When a single change to a program results in a cascade of changes to dependent modules»).

                                3) Оригинал: «What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all
                                programs P defined in terms of T, the behavior of P is unchanged when o1 is
                                substituted for o2 then S is a subtype of T.»
                                Если логика/поведение одних и тех же методов в двух классах будет разной, это изменит поведение программы, а значит нарушит L.

                                4) Соответствие одновременно нескольким интерфейсам — допустимая вещь для Interface Segregation, главное, чтобы эти интерфейсы «правильно» группировали методы по логике/применению. Признаю, я неточно выразился, не упомянув интерфейсы (или «abstract base classes» как в оригинале).

                                Как раз class A implements Foo, Bar, Baz {} это будет нормальный IS, при условии, что части программы, которые используют класс A об этом не знают (DI), а обращаются к нему как к реализации либо интерфейса Foo, либо интерфейса Bar, либо интерфейса Baz.

                                5) Ну, пускай это несколько разные вещи. Как я написал в одном из ответов: «Как именно сделано DI это менее важная вещь, чем то, что в поле или в параметр у нас могут попадать interface либо «родительский» class, у которого могут быть несколько «детей», имеющих «докрученную», более специализированную логику. „
                          • 0

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

                          • –1
                            Этой статья заслужила первое место в моем рейтинге отменной ереси. Браво!
                            • +2
                              Как по мне, вышло слишком сложно. Ради того, чтобы «не заходить за поля тетради», мы реализуем приведенные «спагетти» для простого вызова модального окна. Как показывает опыт, главной причиной ошибок является сложность интерфейса или реализации кода. В данном случае программисту, использующему данный код, нужно помнить обо всём, что нужно поместить внутрь этого гиганта, чтобы вызвать модальное окно, из-за чего значительно увеличивается вероятность ошибки. Стоит ли оно того?
                              • 0

                                Там выше NimElennar упоминал, что хорошо бы использвать facade. Мне казалось это очевидным, но пожалуй допишу в статью. Дело в том, что не обязательно составлять каждый раз окно из самых маленьких кирпичиков. Можно составить из них чуть более крупные, и использовать уже их. Это никак не нарушает принципа, т.к. всегда можно выкинуть эти крупные сборки, и сделать другие (или использовать исходные, маленькие составляющие).


                                То есть, если есть стандартные случаи, то для них можно сделать обобщающие компоненты с более простым интерфейсом. В нестандартных случаях можно будет вернуться ко всей этой сложности.

                                • 0
                                  Весь вопрос — насколько часто понадобятся эти сложности и понадобятся ли вообще. Обычно программистам советуют придерживаться принципа YAGNI
                                  • 0

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

                                  • 0
                                    Понятно, что можно спрятать всю эту логику за более простыми компонентами, которые правильно будут собирать сложный компонент, можно даже для этого использовать что-нибудь вроде этого:

                                    <Modal.YesNoDialog ... />
                                    


                                    Просто местами это выглядит как будто следование слепой вере. В статье много говорится о SOLID, но при этом из всего SOLID мне здесь видится лишь два принципа — SRP и может ISP. Дело в том, что в React как таковом остальные принципы поддержать сложно. LSP — это явно не про то, что один компонент можно заменить другим компонентом, это о том, что при замене некоторого объекта-исполнителя на его наследника мы не получим неожиданного поведения, которого нет в родителе. Здесь нет наследования, в примерах статьи, убрав один компонент, мы избавимся от добавления определенного поведения; заменив компонент на другой мы можем изменить поведение в корне — мы можем заменить затемняющий компонент на любой другой, при этом контракты нашего кода ничего на это не скажут. Это не про LSP вовсе, а максимум о каких-нибудь поведенческих шаблонах проектирования или вообще о паттерне Компоновщик.

                                    Принцип открытости-закрытости говорит нам о том, что чтобы поменять какое-то поведение нам нужно расширить существующий класс путем наследования или композиции вместо того, чтобы менять его изнутри (по крайней мере так звучали первоначальные лозунги). Что мы будем делать с существующими примерами, если завтра нам скажут добавить анимацию при затемнении? Расширить существующий компонент для подложки без того, чтобы менять код компонента ModalBackdrop, у нас явно так просто не получится, придется скорее всего бежать и писать новый или менять старый. Как это повлияет на созданные нами классы-декораторы, которые верно пакуют весь этот набор? Побежим менять и их для нужных реализаций? Т.е. получаем каскадное изменение вместо того, чтобы аккуратно поменять все в одном месте, заменив на нужного исполнителя.

                                    Про DI уже писали выше, в примерах зависимости внутрь компонентов никак не поступают вовсе, потому что их собственно особо и нет, это все Presentational Components.

                                    То есть получается нагородили огород, поговорили о SOLID, но лишь время покажет насколько все это было правильным решением и зачем все это, а может и вообще проще было все-таки делать через props.
                                    • 0

                                      Тут ведь еще какой момент, если мы в одном месте подменим Backdrop на FadeBackdrop, то будет ли везде работать также как работало раньше? К сожалению, в веб без тестирования трудно это утверждать. Где-то кто-то может переопределить стили, и с FadeBackdrop там верстка посыпется. Поэтому делать такого рода гибкость во фронтенде я не вижу смысла. Лучше руками поправить везде и точно знать, где именно ты поправил и где нужно протестить.


                                      DI я понимаю здесь именно в том смысле, что каждый LEGO кусочек не должен предполагать с кем именно он будет взаимодействовать. Очень легко попасть в эту ловушку — пишешь Menu, и подразумеваешь, что внутри будут MenuItem (специальные компоненты). Завязываешь стили внутри, так что кроме MenuItem ничего внутри быть не может. Подход заключается именно в том, что Menu должен быть максимально agnostic относительно своих итемов, чтобы туда можно было положить SuperMenuItem. Это сложно, но это окупается с лихвой. Ну и тем более, Menu сам явно не должен создавать MenuItem, а передавать эту обязанность вызыввающему коду.


                                      Конечно, все принципы здесь применены очень в неявном виде. Здесь скорее дух этих принципов, чем они сами. Например, Seemann пишет, что в общем случае DI is a passing an argument. Именно это я и делаю, передаю дочерние компоненты как параметры (children или аттрибуты).

                                      • +2
                                        Ну то есть получается вся эта архитектура делается для гибкости компоновки, а не для простоты использования и сопровождения и SOLID притягивать здесь смысла было не много. Если мы не можем просто заменить один компонент на другой без уверенности, что все будет работать также, и не можем ничего также расширять, а делаем вышестоящую архитектуру только лишь для того, чтобы указывать что именно нам нужно в данном модальном окне, то почему все это не заменить на обычные props?

                                        <Modal
                                           closeButton={null}
                                           backdrop={true}
                                           title={"Мое модальное окно"}   
                                        >
                                           Добро пожаловать!
                                        </Modal>
                                        


                                        Здесь например, в случае отсутствия title в Modal можно не рендерить заголовок, closeButton можно заменить с null на реальную кнопку с нужной версткой или же вообще сделать его как backdrop типа boolean.

                                        Понятно, что с бо'льшим количеством требованием будет расти и количество props, но в статье как бы предлагается альтернатива, суть которой в комментариях нивелируется самим же автором. Думаю именно эта часть вызывает больше всего вопросов и требует пояснений в самой статье.

                                        • 0

                                          Проблема не столько в количество props (хотя это тоже плохо), сколько в стабильности. Каждый раз, когда нужно будет специальное окно, придется добавлять новую опцию и менять Modal. Весь описанный подход сводится к тому, чтобы не изменять компоненты, которые уже кем-то используются. Ты просто заменяешь те части, которые тебе не подходят, и делаешь любую кастомизацию не ломая уже существующий код.


                                          Все это так важно для фронтенда именно потому, что здесь даже малейшее изменение font-size может сломать всю верстку самым непредсказуемым образом.

                                          • +1
                                            Ну а что если придется менять не Modal, а конкретную эту самую составную часть, которая точно также используется в нескольких местах? Разница получается лишь в меньшей из двух бед, что меняем более маленькую часть вместо большой? Так и в большом компоненте мы можем так же поменять его отдельную маленькую часть и разницы никакой.

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

                                            В плане вот такого выделения отдельных компонентов я вижу необходимость в этом тогда, когда эта часть используется в как минимум трех независимых контекстах, но со схожим использованием кода (оно же «правило трех»). Здесь же все модальное окно по-прежнему может быть представлено и одним компонентом, т.к. по сути контекст один и вне модального окна нигде эти микрокомпоненты использоваться не будут. С других точек зрения пока аргументация не выглядела слишком убедительной, хотя выглядит код может и красиво.
                                            • –1
                                              Ну а что если придется менять не Modal, а конкретную эту самую составную часть

                                              Составные повторно используемые компоненты не должны изменяться, также как и простые. Если что-то не устраивает, то просто заменяешь его. Код составного компонента тривиален, ничего не стоит его заменить.


                                              попробовать решения аля CSSModules,

                                              CssModules очень хорошо ложится на эту концепцию. Стили каждого компонента изолируются, и это создает дополнительную защиту от говнокода. То есть, это вынуждает пользователя не менять компонент, а заменять.


                                              правило трех

                                              Да, что-то в этом роде. Если код используется в 3+ местах, то стоит подумать над созданием переиспользуемого компонента пускаясь во все тяжкие SOLID (это довольно дорого).

                                              • +1
                                                Составные повторно используемые компоненты не должны изменяться, также как и простые. Если что-то не устраивает, то просто заменяешь его. Код составного компонента тривиален, ничего не стоит его заменить.

                                                Вот эту мысль в статье следовало бы выделить. Это уже как-то отличает один подход от другого и является каким-то зерном для дальнейшей мысли. В этом случае правда для компоновки этих составных компонентов при любом изменении придется создавать новый декорирующий компонент, раз мы не меняем ничего из уже реализованного, что со временем может сказаться на DRY.
                                                • 0

                                                  Спасибо за конструктивный диалог, дополнил статью.

                                              • 0
                                                пока трудно дается понять как изменения верстки в одном из компонентов могут так сильно повлиять на всю верстку в целом, что хочется аж усложнять архитектуру

                                                Банальный пример: габариты блоков зависят от многих факторов. Раскладка блоков зависит от их габаритов. В итоге получается, что изменение font-size в одном блоке, может привести к изменению положения не зависящего от него блока, вплоть до полного его исчезновения.

                                    • 0
                                      Кстати, при росте этой иерархии будет тяжелее писать компоненты, основывающиеся на элементах DOM других компонентов — например, скроллы до нужного компонента.

                                      Пример:
                                      Есть у нас CommentPage, на которой отображаются комментарии. Есть компонент CommentList, ответственный за управление логикой списка комментариев (может быть как простое отображение, так и управление списком, опустим этот момент). Соответственно есть компоненты Comment и CommentForm, ответственные за отображение комментария и формирование формы для его заполнения. По логике из статьи CommentForm — это допустим компонент-декоратор, скрывающий компоновку компонентов формы, внутри может быть дюжина маленьких повторно-используемых компонентов, начиная с Form, который будет верно отображать внутри все элементы формы, раздавать внутрь других компонентов какие-нибудь ошибки валидации в общем формате и прочее, заканчивая разными простыми компоненты аля TextBox, Select, CheckBox, Button и другие.

                                      И вот, мы дошли до момента, когда нам надо при нажатии в каком-нибудь комментарии на ссылку «Ответить» проскроллить документ до нужной формы CommentForm. Как это сделать, не нарушив SRP и OCP? Сделать отдельный компонент для скролла. Но нам нужно добраться до нужного DOM элемента из условного Scroller через ref до тега form где-то внутри CommentForm и хорошо если там внутри лишь компонент Form и ничего больше. И хорошо, если можно привязаться на какой-нибудь якорь в документе и перейти к нему, а если вот комментарии на странице выводятся в блоке с абсолютной позицией или с отдельным скроллом, и скроллить надо не документ, а этот блок.

                                      Будь у нас простой компонент CommentForm без внутренних сложностей в виде разбиения на кучу компонентов со своей ответственностью, а простым JSX следующего вида, задача стала бы в разы тривиальнее:
                                      <form ref={el => this.form = el}>
                                         <input type="text" value={this.state.text} ... />
                                         <button ...>Отправить</button>
                                      </form>
                                      


                                      С большой иерархией придется выдумывать как получить этот тег form из компонента Form, который находится внутри компонента CommentForm, и все это управляется аж в компоненте CommentList, который используется и в других местах, где может быть не нужно скроллить до формы, так что по логике из статьи может быть и стоит задуматься создать еще один класс-декоратор, добавляющий эту логику.
                                      • 0

                                        В целом да, если мы абстрагируемся от реальных DOM-элементов, и используем какие-то компоненты, то мы не можем использовать все возможности js и html — только те, которые предусмотрены этими компонентами. Но суть подхода в том, что если нам не хватает возможностей какого-либо компонента, то должно быть легко заменить его. Например, на обычный jsx. Простота замены обеспечивается принципами SOLID, и особенно принципом S — простотой самих компонентов — вам нужно переписать (продублировать) только маленький простой кусочек кода (нужный компонент), тогда как всю остальную логику можно реализовать стандартными компонентами, функционал которых нас в данном случае устраивает.


                                        Применительно к описанному случаю самое простое решение — обернуть форму в простой div, прицепиться к нему через ref и скролить уже до него.

                                        • 0
                                          Ну легко, обернуть в div. А если нужно до определенного input'а внутри этого компонента, то уже не так просто, скорее всего придется переписать весь компонент-компоновщик и уже там обернуть input в div. Можно и кучу refs надобавлять, только тогда «потечет» инкапсуляция. Суть в том, что чтобы прибегнуть к каким-то конкретным решениям в повторно-используемых компонентах, иногда проще отказаться от созданной архитектуры в целях практичности, чем получить в папке проекта компоненты, подобные следующим: CommentForm, ScrolledCommentForm, ScrolledToNameCommentForm, ScrolledToNameCommentFormWithGreenButtons и т.д.

                                          В целом, идея далеко не нова, о ней даже говорится в одном из самых первых гайдов по реакту в официальной документации, и основной посыл статьи остается довольно очевидным. Но сама суть сразу создавать новый класс как только нам понадобилось что-то изменить в отображении или поведении компонента — немного выходит за рамки и может привести к каше. Если не нужно в корне новое поведение, мне проще поддержать концепцию DRY и ввести новый props с указанием defaultProps, чтобы не разломать существующий код, чем городить еще раз несколько похожих классов.

                                          Все это конечно актуально, пока мы говорим о presentational components, без какой-то внутренней сложной логики. Как Вы работаете с состоянием приложения и контейнерами тоже было бы интересно почитать.
                                          • 0

                                            В примере из доков нарушен принцип D, и поэтому этот пример вообще никак не связан со статьей (довольно распространненный паттерн, кстати). Там UserInfo сам принимает решение о рендеринге Avatar, поэтому вызывающий код не может подменить Avatar на что-либо еще, не изменив UserInfo. Только соблюдение всех 5 принципов дает тот эффект, о котором я говорю — возможность заменять компоненты, но не изменять.


                                            По поводу ScrolledToNameCommentFormWithGreenButtons, проблема, конечно есть. Но не нужно создавать новый компонент, если он используется только один раз. Просто замените его на div. Вот если у Вас в пиложении будет 2-3+ случаев, где нужно будет скролить до Name с зеленой кнопкой, то да, такой контрол стоит создать. Но обычно, есть общее правило, и есть единичные исключения.

                                            • 0
                                              В примере из доков нарушен принцип D, и поэтому этот пример вообще никак не связан со статьей (довольно распространненный паттерн, кстати). Там UserInfo сам принимает решение о рендеринге Avatar, поэтому вызывающий код не может подменить Avatar на что-либо еще, не изменив UserInfo.

                                              Там пример в принципе оторван от контекста, скорее показан не переиспользуемый компонент вовсе, а Вы спешите поддержать DI и вынести Avatar из UserInfo. Так придете к тому, что в проекте будут одни компоненты-однострочники, которые оборачивают children в какой-нибудь div. Что-то должно собирать всю эту логику, поэтому и пишутся компоненты-обертки и поддерживать DI не всегда имеет смысл.

                                              И Ваши слова начинают разниться с тем, о чем говорилось выше. Если у нас одна сборка компонентов использовалась в нескольких местах, а потом понадобилось в другом месте добавить такую функцию, как скролл, и Вы заранее не знаете, где она будет нужна, а где нет, то рекомендовалось заменить компонент-обертку на другую, чтобы не затронуть существующий код. Но почему-то далее говорится о том, что не нужно этого делать, если данная проблема встречается всего в одном месте. Но я же хочу использовать не весь багаж из 15 классов, а получить от архитектуры простой для использования интерфейс. Либо мне придется залезть внутрь класса-обертки, разобраться, что он рендерит, скопировать его содержимое оттуда и вставить с изменениями в место, где понадобилось новое требование, либо напишу новую обертку и получу еще один класс со страшным названием ScrolledCommentFormWithNoHeaderAndPreview, описывающим конфигурацию компонентов внутри этой сборки. Первое черевато тем, что стоит этой логике повторно появиться в другом месте, то придется вспоминать где же я там копировал этот код, чтобы сейчас выделить класс. Плюс ко всему, если понадобится изменение компонента-обертки, которое должно затронуть всё её использование, то про это место легко забыть. Потому тут на помощь приходит DRY и мы скорее пойдем вторым путем. Однако, в случае необходимости глобального изменения (например, какой-то компонент стал ненужным), это спасает нас не сильно — придется менять все классы-обертки вместо того, чтобы изменить что-то в одном месте. Плюс ко всему, наличие кучи компонентов-оберток в проекте с такими страшными названиями будет вводить в недоумение разработчиков, не работающих ранее с этой частью приложения — задача «какую обертку выбрать» становится не самой тривиальной.
                                              • 0

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

                                                • 0
                                                  Правильно ли я понял Вашу мысль — есть у нас задача с созданием, редактированием и ответом на комментарии в списке. В зависимости от типа действия нужно изменить заголовок формы и надпись на кнопке — при создании в заголовке написать «Создание комментария» и на кнопке написать «Добавить», и также для редактирования и ответа на комментарий. У нас в системе есть иерархия компонентов для работы с формами. Для данной задачи Вы предлагаете создать три класса CommentCreateForm, CommentEditForm и CommentReplyForm, каждый будет заполнять верно заполнять заголовки и кнопки, правильно я понимаю? Просто пытаюсь понять Вашу точку зрения.
                                                  • 0

                                                    Обертка — это не более чем синтаксический сахар для рендеринга других компонент, поэтому не будет зазорным объявить в ней все свойства, которые есть у компонентов которые она рендерит. В описанном случае, если эти три формы отличаются только надписями, достаточно будет одной обертки которая принимает параметрами заголовок и текст кнопки.

                                                    • 0
                                                      А если форма используется для комментирования разных сущностей в разных компонентах-контейнерах, например, комментирование видео, фотографий, новостей, каких-нибудь прочих записей? Структура комментария одинаковая, но, допустим, надписи разные в каждом из случаев. Будете использовать общий CommentForm или все же выделите класс для каждого из вариантов использования?
                                                      • 0

                                                        Можно передавать в обертку сами компоненты (как делает vintage в других ветках), но тут нужно смотреть, останется ли что-то от самой обертки. Если требуется высокая степень вариативности, так что все тело обертки приходится кастомизировать, то такая обертка просто не нужна.


                                                        Вообще, я не люблю обертки, с ними возникает куча таких вот скользких вопросов. Проще всегда использовать полную обойму простых компонент и всегда иметь гарантированно максимальную гибкость.

                                                        • 0
                                                          Мне обертки нравятся только потому, что они позволяют отвлечься от сложных деталей реализации и позволяют инкапсулировать в одном месте логику компоновки компонентов — если что-то нужно добавить или изменить так, чтобы это повлияло и на другие места приложения, то достаточно сделать это в одном месте. Полный набор компонентов такого делать не позволит, придется бегать по всей системе с Find Usages. Также придется всегда помнить о том, какие компоненты есть в иерархии, но эта проблема скорее больше упирается в спеки и доки проекта, с которыми каждому разработчику, работающему с требованием, придется знакомиться в любом случае.

                                                          В случае создания нескольких оберток вместо одного компонента с определенными параметрами, логика начинает «расплываться» на несколько компонентов и мы теряем возможность легко вносить изменения в одно место приложения, т.к. как раз от этого и пытаемся отказаться в данной архитектуре. Т.е. получается, стремясь поддержать SOLID, мы забываем о DRY и YAGNI. Этим мне видится минус данного решения.
                                                          • 0

                                                            Да, все верно, это то, чем приходится расплачиваться за стабильность. Просто нужно выбирать, что важнее — стабильность или возможность менять все приложение одной строчкой. Если последнее делается специально — это хорошо. Но чаще это делается не специально — хотел поменять только в данном сценарии, а поломал все приложение.

                                                            • 0
                                                              Согласен, однако, для избежания непредвиденных изменений есть ряд и других практик изолирования изменений. В случае добавления нового свойства к компоненту можно добавить также базовое значение, чтобы поддержать существующий код. В случае не JS можно создать перегрузку метода. Тут главное помнить о SRP и не начать пихать в компонент все, что не попадя.
                                            • 0

                                              Ну и да, Name тоже можно обернуть в div. Если используется обертка, которая сама создает Name (кстати да, обертки тоже нарушают принцип D, поэтому их нужно использовать осторожно), то эту обертку можно заменить на ее содержимое, и таки обертнуть Name. То есть, общий принцип простой — если не устраивает компонент, просто копируешь его исходники (чем меньше компоненты, тем меньше приходится копировать). Если такое потребовалось дважды и более, есть смысл создать новый компонент.

                                      • 0
                                        Ну да ладно, добавляем еще параметр:


                                        прямо аларм о том, что начал прорисовываться антипаттерн telescoping constructor
                                        • 0
                                          Сам недавно писал такое окно на js. Пол года где-то писал. Реализовал все возможные инпуты и поведения формы. Как-то никаких проблем не заметил. есть конструктор Form, задаются параметры, где Form([массив инпутов], функция-обработчик, {объект — свойства формы}). Всё! Придумывай сколько угодно инпутов по единому базовому шаблону с неограниченной возможностью кастомизации параметров и вешай сколько угодно свойств и методов на саму форму. При чем вся моя философия повторного использования кода сводилась к следующему: видишь кусок кода повторяющийся больше двух раз — выноси в функцию. Я не пытаюсь что-то опровергнуть, но я не вижу большой пользы от всей этой конструкции под названием SOLID. По крайней мере статья этого не раскрывает.
                                          • 0

                                            Если в каком-то случае Вам нужна была новая фича от Form, которой не было предусмотрено. Что в этом случае Вы делали?

                                            • 0
                                              Например, понадобилось, чтобы форма была встраиваемая в основной контент. Просто в конструктор передаешь третьим параметром: {parent: selector}, то есть родительскую ноду, в которую надо встроить форму и затем рендеришь в нее, а не в popup. Как-то так. Если нужно убрать кнопки, оставив крестик добавляешь что-то типа {parent: selector, controls: false}. Если нужна какая-то особая кнопка то используешь инпут типа html — просто контейнер для гипертекста и в него вставляешь нужные кнопки, размещая их как надо при помощи стилей. Если нужна кастомная стилизация, то добавляешь {parent: selector, controls: false, class: 'myCustomClass'}, затем добавляешь этот класс к форме $form.addClass(class). Думаю смысл понятен.
                                              • 0
                                                добавляешь что-то типа {parent: selector, controls: false}

                                                Это описано в секции статьи "Как делать не надо":


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

                                                Описанный подход решает проблему следующим образом:


                                                Ключевой момент здесь в том, что мы вместо изменения компонента, который используется во многих местах, просто заменять его на другой. Это важно для компонентов в веб, потому что любое изменение в стилях может повлечь нарушение верстки в каких-то обстоятельствах использования компонента. Единственный надежный способ обезопасить себя от этого — не изменять однажды написанные компоненты (если они многократно используются).
                                                • 0
                                                  «Это описано в секции статьи „Как делать не надо“». Но я так сделал. накатал 3к строк кода и ничего, по необходимости навешиваю всё новые свойства, когда надо. Поэтому я и не понял, почему рабочий метод забраковался и вместо него предлагается некая сомнительная конструкция. Давайте просто сравним подходы в контексте приложения. Надо решить упомянутую проблему с кнопками.
                                                  До:
                                                  new Form(inputs, function(results){
                                                   send(results);
                                                  });
                                                  

                                                  После:
                                                  new Form(inputs, function(results){
                                                   send(results);
                                                  }, {showControls: false});
                                                  

                                                  Напишите свой пример До и После.
                                                  • +1

                                                    До (будем считать, что использованы некоторые обертки):


                                                    <Form onSend={this.onSend)} isOpen={this.state.open}>
                                                       <FormHeader>My form</FormHeader>
                                                       <FormCloseButton onClick={this.onClose()} />
                                                           {inputs}
                                                       <StandardFormButtonPanel />
                                                    </Form>

                                                    После:


                                                    <Form onSend={this.onSend)} isOpen={this.state.open}>
                                                       <FormHeader>My form</FormHeader>
                                                           {inputs}
                                                       <StandardFormButtonPanel />
                                                    </Form>

                                                    Фишка в том, что я убрал CloseButton для моей формы, но при этом не поменял ни один из повторно-используемых компонентов. То есть, никакой код в других местах приложения, который использовал Form и т.д. не мог поломаться даже теоретически — мы не добавляли опций, не изменяли кода Form.

                                                    • 0
                                                      Так об обертках то и речь. Именно их Вы забраковали, а не внутреннюю реализацию. Моя внутренняя реализация выглядит практически так же.
                                                      var formHTML = showControls ? 
                                                       '<form><controls></controls></form>' :
                                                       '<form></form>' 
                                                      $('#form').html(formHTML);
                                                      

                                                      То есть в моем случае я просто добавляю кейс на уровне if или switch. Вы же городите новый модуль. При чем он городится точно так же по мере возникновения необходимости, а не заранее, ведь изначально неизвестно нужно ли будет его скрывать или нет.
                                                      В итоге мы имеем:
                                                      function Form(inputs, handler, options){
                                                       var inputsHTML = getInputsHTML(inputs);
                                                       var formHTML = showControls ? 
                                                        '<Form onSend={this.onSend)} isOpen={this.state.open}>
                                                         <FormHeader>My form</FormHeader>
                                                         <FormCloseButton onClick={this.onClose()} />
                                                             {inputsHTML }
                                                         <StandardFormButtonPanel />
                                                        </Form>' :
                                                        '<Form onSend={this.onSend)} isOpen={this.state.open}>
                                                         <FormHeader>My form</FormHeader>
                                                             {inputsHTML  }
                                                         <StandardFormButtonPanel />
                                                        </Form>'  
                                                        $(body).html(formHTML);
                                                      }
                                                      
                                                      new Form(inputs, handler, {showControls:false})
                                                      
                                                      • 0

                                                        В вашей реализации нарушены буквы O и D из SOLID:


                                                        O (open-closed) — никогда, ни при каких обстоятельствах не модифицируем код компонентов, которые часто используются;
                                                        D (dependency inversion) — решение о том, какой из компонентов будет использован в каждом случае, должен принимать вызывающий код.

                                                        Из-за того, что нарушена D пришлось нарушить S. Решение о рендеринге FormCloseButton принимает Form, а не вызывающий код. Поэтому, когда нам понадобилось изменить это поведение, нам пришлось менять Form. А изменение повторно используемого компонента это зло.


                                                        В целом, Ваш Form по сути является оберткой, о которых я пишу. Однако, обертка не должна изменяться. Если нужно другое поведение, то нужно заменять обертку.


                                                        По поводу скрытия кнопки в моем варианте все проще:


                                                        <Form onSend={this.onSend)} isOpen={this.state.open}>
                                                           <FormHeader>My form</FormHeader>
                                                           {this.state.showCloseButton && <FormCloseButton onClick={this.onClose()} />}
                                                               {inputs}
                                                           <StandardFormButtonPanel />
                                                        </Form>
                                                        • 0
                                                          Погодите меня штрафовать за нарушения. Я пока в целом не понял, чем я так провинился и чем ваш подход лучше. В итоге, так и не ясно от куда взялось this.state.showCloseButton. Кто и через что этот state туда засунул? В этом же ключевой момент. И для чистоты эксперимента давайте может чистый js и html использовать (не считая jquery). А то я может какой-то ключевой момент упускаю.
                                                          • 0

                                                            Тот код, который я приводил, это код компонента приложения. Он собирает из повторно используемых компонентов конкретное окно, для решения конкретной задачи. 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>`;
                                                            }

                                                            Это конечно все скорее псевдокод, но идея думаю ясна.

                                                            • 0
                                                              Допустим. Но пока выгоды не вижу. Раз форма о них ничего не знает, что делать в ситуации, когда при сабмите инпуты проходят валидацию и надо мигнуть, если поле пустое.
                                                              • 0

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


                                                                Взаимодействие (вроде валидации) — одна из самых сложных тем в этом подходе. Допустим, что html-валидацию мы не можем примнить, и нам нужна кастомная. Варианты тут следующие:


                                                                1. Вынести все взаимодействие в вызывающий код. То есть, тот кто собирает форму подписывается на onSubmit, проверяет там значения полей, и если нужно подсветить какое-то поле, передает ему параметр invalid=true, или добавляет новые компоненты, которые рисуют выделение и показывают сообщение. Обычно, именно этот подход и используется. Допустим, выше по клику на FormCloseButton по умолчанию ничего не происходит. Нужно самим подписаться на клик, и самим закрыть форму (передать isOpen в Form).
                                                                2. Сделать обертку, которая будет генерировать некоторые стандартные формы, и добавлять к ним стандартную валидацию, как описано в 1. Этот подход опасен тем, что мы начнем менять эту обертку. Если обертка часто используется, то она не должна меняться.
                                                                3. Предъявить какие-то требования к дочерним компонентам. Form может потребовать, чтобы дочерние компоненты были не произвольными, а реализовывали определенный интерфейс (имели функцию validate, например). Тогда форма сможет пробежаться по children и выполнить валидацию.
                                                                4. Использовать общий контекст. Мы создаем некоторый объект (контекст), который передаем в Form, и во все дочерние компоненты-инпуты. Каждый компонент умеет работать с контекстом (это часть его интерфейса). В контексте есть некоторый механизм, через который обеспечивается взаимодействие. В первом приближении это архитектура Flux.
                                                                • 0
                                                                  Я использую пункт 3. Но тогда рушится вся концепция SOLID. Ведь форма начинает знать о свойствах и методах инпутов.
                                                                  И раз уж в этом подходе «одна из самых сложных тем», где я проблемы даже не встретил, то я просто создал отдельный базовый класс Input, от которого отнаследовал остальные инпуты, откастомизировав их, а сама Form использовала их прототипные методы вроде getValue(), getHTML(), blink() и т.д. Хочешь добавить свой метод, например, onBlur(), не вопрос, передавай его в параметр инпута, типа new Form([{droplist:{onBlur: blurHandler}}], handler, opts). Я считаю в этом как раз наоборот вся красота и простота подхода, а не его недостаток. Да и собственно в статье эти недостатки описаны как-то пространно, вроде «это „попахивает“», «грозит появлением регрессий». Я даже не понимаю о чем речь. Как будто меня пугают привидениями.
                                                                  • +1

                                                                    Смотрите, когда мы просто передаем любой компонент — мы уже задаем некоторый интерфейс, и родитель что-то о своих детях знает. В React он знает что дети — react-компоненты (они имеют функцию render и др.). Если Form потребует чуть более специфичный интерфейс для детей (не только функция render, но и еще функция validate), то концепции в целом это не нарушит. Просто создаст некоторые ограничения по использованию Form.


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


                                                                    ShowDialog(title, content);

                                                                    Потом мы начали работать над новым требованием, и нам понадобился диалог без затемнения фона. Мы добавили необязательный параметр showOverlay, и использвали его в фиче Б так:


                                                                    ShowDialog(title, content, false);

                                                                    Но по невнимательности в коде ShowDialog эту опцию реализовали следующим образом:


                                                                     ShowDialog(title: string, content: string, showOverlay?: boolean) {
                                                                        // ...
                                                                        if (showOverlay) {
                                                                            // render overlay
                                                                        }
                                                                    }

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


                                                                    Если использовать подход описанный в статье, то добавление новых фич гарантированно никак не повлияет на ранее реализованные фичи (по крайней мере на уровне компонент).

                                                                    • 0
                                                                      Может, я не так все понимаю. Начинает разработчик делать форму. Сначала у него что-то вроде
                                                                      <container>
                                                                       <form>
                                                                        <inputs></inputs>
                                                                        <controls></controls>
                                                                       </form>
                                                                      </container>
                                                                      .
                                                                      Дальше у него возникает необходимость убрать оверлей:
                                                                      
                                                                      var formHTML =  '\
                                                                       <form>\
                                                                        <inputs></inputs>\
                                                                        <controls></controls>\
                                                                       </form>';
                                                                      var  fullHTML = overlay ?
                                                                       '<container>' + formHTML + '</container>' :
                                                                       formHTML;
                                                                      

                                                                      И так далее. То есть изначально у разработчика есть один большой блок, затем он дробится по мере разработки, когда возникает необходимость добавить новых фич. То есть влезание в код часто используемого компонента — это неизбежность. А как его иначе улучшить? Или как его разбить на меньшие компоненты?
                                                                      Тем более ветвление никуда не девается. Ну было оно раньше внутри компонента, а стало межкомпонентное. И что мы выиграли?

                                                                      На самом деле у меня действительно есть проблема, когда в форму вваливатеся куча параметров и их все нужно обработать, при чем каждый влияет на остальные особым образом. И от этого if-else ада я пока не нашел способа избавиться. Думал SOLID поможет, но он как-то не об этом.
                                                                      • 0

                                                                        Вы путаете повторно используемые компоненты и компоненты приложения. form, container — это повторно используемые, а formHTML — это код компонента приложения, он не используется повторно. Он реализует конкретную фичу в единственном экземпляре. Вот это условие overlay ? нужно только в том случае если Вам нужно динамически менять поведение (показывать/не показывать) overlay. Обычно такой динамики не нужно, и если компоненту не нужен overlay, то он просто его не использует.


                                                                        p.s. SOLID как раз об этом. Но если у вас много динамической логики (какие-то настройки), то тут да, где-то эти if-ы должны появиться.

                                                                        • 0
                                                                          Хорошо, вот пример моей проблемы:
                                                                          Конструктор
                                                                          Input({
                                                                           inputMode:true,
                                                                           checkMode:true,
                                                                           multiSelect:true
                                                                          })
                                                                          

                                                                          Внутренняя реализация:
                                                                          function Input (opts) {
                                                                           if(inputMode){
                                                                            if(checkMode){
                                                                             if(multiSelect) {/*case 1*/}else{/*case 2*/}
                                                                            } else {
                                                                             if(multiSelect) {/*case 3*/}else{/*case 4*/}
                                                                            }
                                                                           } else {
                                                                            if(checkMode){
                                                                             if(multiSelect) {/*case 5*/}else{/*case 6*/}
                                                                            } else {
                                                                             if(multiSelect) {/*case 7*/}else{/*case 8*/}
                                                                            }
                                                                           }
                                                                          }
                                                                          

                                                                          Можно ли кадринально изменить этот код с помощью SOLID?
                                                                          • 0

                                                                            Могу предположить, что вариантов не 8. checkMode очевидно не имеет смысла для input type="text", также как и multiSelect. Опишите, пожалуйста, подробнее задачу (что значат эти опции). Нужно ли динамически (на основе каких-то данных) выбирать соответствующую опцию, или нужный вариант известен на этапе написания кода?


                                                                            В целом, для каждого варианта, который отличается от других, скорее всего нужно сделать отдельный компонент. Потом можно сделать обертки, которые будут выбирать тот или иной компонент на основе передаваемого параметра.

                                                                            • 0
                                                                              Пусть инпут будет droplist, multiSelect — множественный выбор с отображением выбранного свреху над дроплистом, checkMode — чекбокс напротив каждого пункта дроплиста, inputMode — в поле дроплиста можно вводить своей значение, а из пунктов находится наиболее подходящий. Пусть кейсов даже будет немного меньше. Все равно легче не становится.
                                                                            • 0
                                                                              Если по SOLID, то это предполагается, что будет несколько разных компонентов/имплементаций.

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

                                                                              Да, возможно будет небольшое дублирование, но с другой стороны: что лучше месево из IF или дублирование небольших участков?
                                                                              Если большой кусок дублируется, его можно всего в отдельный компонент вынести или в какой-то хелпер/утиль.
                                                                              • 0

                                                                                Дублирование всегда можно вынести в отдельный компонент/враппер. Если компоненты будут достаточно маленькими, то дублирование кода внутри разных компонентов будет минимальным.

                                                                                • 0
                                                                                  Но проблема то никуда не денется. Ну будет у нас вместо одного droplist(opts), восемь конфигураций типа droplistCheck(), droplistMS(), droplistInput() и так далее, всего 8 компонентов.
                                                                                  • 0

                                                                                    Можно сделать droplist(opts) обертку в дополнение к этим 8 компонентам, если есть острая на то необходимость (он будет просто создавать droplistCheck, droplistMS и т.д.). Только этот droplist не должен меняться.


                                                                                    Ну а в целом да, будет 8 компонентов. Бойлерплейта в этом подходе много, это факт. Именно поэтому его нужно использовать только там, где это нужно.

                                                                                    • 0
                                                                                      То есть с появлением нового режима, мне нужно городить droplist2(opts)? Лишь бы не трогать старый? Какой-то оверкилл. И почему сейчас «Можно сделать droplist(opts)», а раньше (в статье) это было нельзя?
                                                                                      • 0

                                                                                        В статье говорится про компоненты-обертки, это именно оно. И да, городить новый droplist2, чтобы не трогать. Ну, на самом деле выбор, конечно, всегда есть. Просто изменение droplist это боль — нужно проверять что ничего не ломаешь. Ты выбираешь — словить эту попа-боль, или просто сделать другую обертку. Вообщем, жизнь боль в любом случае: ))

                                                                                        • 0
                                                                                          Не совсем так.

                                                                                          Если нужно добавить новое поведение – нужно создать новый компонент.

                                                                                          Если нужно изменить поведение только в определенных местах – нужно создать новый компонент (с новым поведением).

                                                                                          Если нужно исправить / изменить поведение текущего везде – нужно просто взять и изменить текущий компонент. Новый компонент создавать нету необходимости.
                                                                                  • 0
                                                                                    Поясните свою точку зрения на счет того, что проблема останется.

                                                                                    Лично для меня в приведенном примере основная проблема в том, что у него высокая цикломатическая сложность. Этот компонент крайне тяжело отлаживать, еще сложнее тестировать и невероятно сложно поддерживать.
                                                                                    – это некий «God Component», который может принимать любую форму в зависимости от входных параметров.

                                                                                    Разбитие на 8 компонентов вообще без условных операторов внутри компонента спасает от всех этих проблем. Но за это вам придется заплатить небольшим дублированием кода и увеличением количества файлов. Но это приемлемая плата.

                                                                                    Каждый из компонентов будет иметь единственную ответственность, которую будет очень легко понять, протестировать и отладить.
                                                                                    Более того, ошибка в таком маленьком компоненте будет сразу видна даже без запуска и без дебаггера.
                                                                                    Найти проблемный компонент и причину проблемы будет невероятно просто. Внести правку – еще проще (не нужно учитывать все условия отрисовки и сломать что-то в другом месте будет невозможно).
                                                                                    • 0
                                                                                      не нужно учитывать все условия отрисовки и сломать что-то в другом месте будет невозможно

                                                                                      Если отрисовка условная в принципе, то условия нужны будут всегда. Где-то что-то не учёл — сломалось. Вопрос лишь в том, как много модет сломаться и и как часто возникает необходимость менять.

                                                                                      • +1
                                                                                        Я абсолютно согласен. Добавлю только то, что чем больше условных операторов в отрисовке компонента, тем больше вероятность, что что-то пойдет не так.

                                                                                        Более того, если компонент имеет много ответственности, он явно будет использоваться во многих местах, а значит ошибка в нем может сломать очень много.
                                                                                      • 0
                                                                                        «это некий «God Component»,» Прямо в точку. Это именно то, что и предполагалось сделать. Эдакое параметрическое программирование: вводишь конфигурацию — получаешь нужный компонент.
                                                                                        Повторюсь у меня нет проблем с отладкой, тестированием (тесты отсутствуют), развитием и поддержкой. Меня смущают макароны из if-else конструкций, которые выглядят не эстетично. Хотелось что-то более феншуйское.
                                                                                        Все принципы SOLID похожи на библейские заповеди, как хочешь так и трактуй. В итоге начинаются холивары, вроде, как надо: element.render() или render(element)?
                                                                                        • 0
                                                                                          Если вам нужно сделать «God Component», то SOLID вам точно не поможет. SOLID это набор принципов о том, как избежать God Objects / God Components и писать код, который можно будет легко поддерживать и тестировать.

                                                                                          НО, в целом, можно пойти следующим путем:
                                                                                          1. вы заводите все те же 8 компонентов.
                                                                                          2. вы заводите GodComponent, который будет по входящим параметрам выбирать один из нужных 8.
                                                                                          3. вы пишите функцию-маппер (своего рода hash-function) от входящих параметров, которая на выходе возвращает Класс нужного компонента.
                                                                                          4. вы инстанциируете компонент с параметрами от GodComponent.

                                                                                          const COMPONENTS = {
                                                                                            'imput_check_multi': InputCheckMultiComponent,
                                                                                            'imput_ncheck_multi': InputNotCheckMultiComponent,
                                                                                            'imput_ncheck_single': InputNotCheckSingleComponent,
                                                                                          ...
                                                                                          }
                                                                                          
                                                                                          function getComponent(params) {
                                                                                             const name = [];
                                                                                             name.push(params.input ? 'input' : 'static');
                                                                                             name.push(params.check ? 'check' : 'ncheck');
                                                                                             name.push(params.multi ? 'multi' : 'single');
                                                                                          ...
                                                                                             return COMPONENTS[name.join('_')];
                                                                                          }
                                                                                          


                                                                                          или вместо Класса компонента возвращать Функцию, которая будет инстанциировать компонент – это уже как вам больше нравится.
                                                                                          • 0
                                                                                            И ситуация становится еще хуже. Теперь, вместо того, чтобы просто лазить по простыне if-elseов в одном месте, я буду лазить по 8-ми кускам кода, к которым отсылает новый god-component.
                                                                                            Честно говоря, мне кажется, что я уже использую SOLID подход, но мы как-то не то обсуждаем. Например chekbox() в checkMode или input() в inputMode это такие же компоненты, как и droplist(), он их использует внутри себя по своему усмотрению. И в этом есть смысл: повторное использование, дочерний элемент ничего не знает о родителе, ну и так далее. Но в статье это как-то на столько извращено и перековеркано, что рациональное зерно утеряно. Остались только радостные возгласы о том, как стало легче жить и светлое будущее не за горами.
                                                                                            • 0
                                                                                              И ситуация становится еще хуже. Теперь, вместо того, чтобы просто лазить по простыне if-elseов в одном месте, я буду лазить по 8-ми кускам кода
                                                                                              На самом деле эта ситуация на много лучше, чем простыня условных операторов.

                                                                                              Но если вы привыкли, чтобы весь код приложения был в одном месте, вам будет по-началу непривычно/неудобно.

                                                                                              Но это лечится практикой такого подхода и дальше это станет даже удобнее, чем все в одной куче.

                                                                                              1 js файл на 500+ строк кода, который содержит в себе кучу логики и условного рендера – это худшее, что может быть для поддержки. Это прям как открыть какой-то проект на php 4.x, когда все писали в одной куче.

                                                                                              Если ваша цель написать один раз и выкинуть – можно в одном файле.
                                                                                              Если же вы хотите поддерживать в будущем или тем более работать в команде – разделяйте на несколько модулей-файлов.
                                                                                              • 0
                                                                                                Я пробовал подебажить одну библиотеку собранную вебпаком. Скажу это то еще приключение. Когда модуль бывает состоит из одной строчки кода, а внутри вызывает другой модуль в итоге мы получаем незабываемое путешествие по измерениям. И там такой мэппинг, что Алисе даже и не снилось в ее стране чудес. А когда находим нужное место, от которого якобы что-то зависит, оказывается, что это не то место и путешествие начинается заново. Через час другой такой отладки, отпадает всякое желание работать с таким кодом.
                                                                                          • 0
                                                                                            Эдакое параметрическое программирование: вводишь конфигурацию — получаешь нужный компонент.
                                                                                            это уже из паттернов проектирования
                                                                                            и называется он «фабрика»
                                                                                            • 0
                                                                                              Фабрика это как раз multiSelectInputDroplist(), а не new с параметрами в конструкторе.
                                                                                        • 0

                                                                                          Тут лучше использовать другой способ декомпозиции: [MultiSelect|SingleSelect] > [FullList|FilteredList] > [PlainItem|CheckedItem]


                                                                                          Итого компонент: 2*3 = 6
                                                                                          Итого комбинаций: 2^3 = 8

                                                                • 0

                                                                  Ну и третий вариант...


                                                                  Реализация формы:


                                                                  $my_form $mol_view
                                                                      submit?event null
                                                                      sub /
                                                                          <= Head $mol_view sub <= head /
                                                                          <= Close $mol_button_minor sub /
                                                                              <= Close_icon $mol_icon_cross
                                                                          <= inputs /
                                                                          <= Controls $mol_row sub /
                                                                              <= Ok $mol_button_minor title \Ok
                                                                              <= Cancel $mol_button_minor title \Cancel

                                                                  Использование до:


                                                                  $my_app $mol_page
                                                                      body / <= Sign_in $my_form
                                                                          title \Sign in
                                                                          submit?event <=> sing_in?event null
                                                                          inputs /
                                                                              <= Login $mol_string
                                                                              <= Password $mol_string

                                                                  Использование после:


                                                                  $my_app $mol_page
                                                                      body / <= Sign_in $my_form
                                                                          title \Sign in
                                                                          submit?event <=> sing_in?event null
                                                                          Close null
                                                                          inputs /
                                                                              <= Login $mol_string
                                                                              <= Password $mol_string

                                                                  Итого:


                                                                  1. Мы не вводили 100500 параметров в конструкторе.
                                                                  2. Мы не вводили 100500 компонент незначительно отличающихся друг от друга
                                                                  3. Мы кастомизировали форму заменив один из её вложенных компонент.
                                                                  • 0

                                                                    То есть, в определении формы объявлены некоторые слоты, которые имеют значение по умолчанию и которые можно заменить? Очевидный недостаток такого подхода — нельзя добавить что-то в форму, что там не объявлено. Но в целом да, передавать компоненты через пропсы один из вариантов.

                                                                    • 0

                                                                      Наследуемся и добавляем, какие проблемы?


                                                                      $my_resetable_form $my_form
                                                                          Controls $mol_row sub /
                                                                              <= Ok -
                                                                              <= Cancel -
                                                                              <= Reset $mol_button_danger title \Reset

                                                                      Разумеется, лучше добавить слот constrols, для собственно списка контролов, чтобы не переопределять Controls. Добавление слотов — совершенно безопасно.

                                                                • 0
                                                                  Самое простое, что можно сразу сказать, почему Ваш подход хуже, чем у автора — сложность сигнатуры конструктора. Чтобы понять что такое options, нужно зайти и внимательно прочесть summary-комментарии к Вашему конструктору. Если их нет — придется разбираться с реализацией конструктора, чтобы понять что туда нужно посылать и к чему это приведет. По конструктору может быть не ясно зачем задается параметр, вот пример:

                                                                  class Form {
                                                                     constructor(type) {
                                                                         this.type = type;
                                                                     }
                                                                     ...
                                                                  }
                                                                  


                                                                  Зачем здесь type? Неизвестно, он просто сохраняется для дальнейшего использования внутри класса. Что будет делать разработчик, незнакомый с данным классом? Пойдет разбираться с другими методами, чтобы понять что же это за type такой и для чего он нужен. И обертки тут даже не спасут — если разработчику придется создавать свою, то ему все равно придется ознакомиться с реализацией, чтобы понять, как правильно эту обертку написать.

                                                                  Знакомиться с реализацией автора намного проще, во многом достаточно прочесть просто названия компонентов, чтобы понять что они делают и что туда слать. Другие нюансы легко уточняются за счет того, что компонент поддерживает SRP (S из SOLID) — у него одна ответственность — рендеринг конкретной части интерфейса, и эта часть довольно атомарна и тривиальна, класс занимает минимум строчек кода.
                                                                  • 0
                                                                    Так вместо сигнатуры конструктора надо будет учить сигнатуру компонентов.
                                                                    • 0
                                                                      В которых разобраться в разы проще по описанным выше причинам. Если нужен минимальный функционал, то не нужно будет изучать весь ваш большой класс Form, а достаточно просто ознакомиться с несколькими простыми интерфейсами.
                                                                      • 0
                                                                        Кто-то запрещает делать обертки на часто используемые компоненты?
                                                                        • 0
                                                                          Опять же, чтобы реализовать обертки, в начале нужно разобраться с этим гигантским классом. Если Вы работаете в проекте один, то таких проблем может быть Вы и не встретите, если конечно не забросите проект на год, а потом не попробуете к нему вернуться. Но опыт подсказывает, что, например, новые сотрудники разбираются с огромными участками кода намного дольше, чем с маленькими разбитыми блоками как у автора, пусть даже их и несколько.

                                                                          Плюс ко всему, выше можете почитать рассуждения по поводу оберток и к чему приводит их повсеместное использование.
                                                          • 0
                                                            полгода попап на html? Вот каковы шансы, что в самой безумной статье, попадется самый безумный комментарий, при том не от автора статьи?
                                                            • 0

                                                              Уверен, какой-нибудь FullCalendar который пытается уметь всё на свете писали ещё дольше.

                                                          • 0
                                                            Позже понадобилось показывать окна без «крестика» в правом верхнем углу, без заголовка либо с другим заголовком, с другими отступами от краев окна, какие-то окна нужно было закрывать по щелчку во вне, а какие-то нет…

                                                            Ну так ноги у проблему растут из проектирования дизайна как я понимаю. Если нет общей дизайн-системы/style-гайда/брендбука/etc, то нормального переиспользуемого компонента не будет. Проще, быстрее, дешевле создать новый компонент под сегодняшнее настроене дизайнера, даже если в нём будет 70% копи-пасты, чем городить универсального монстра.
                                                            И думаю то что не предугадал дополнительные состояния, поведения, кнопки — это не проблема разработчика. Разработчик не ванга. Он не должен предугадывать, он должен просто реализовать в коде то что уже(!) продумали специально обученные люди.
                                                            Это я сейчас в контексте вёрстки, если что)
                                                            • 0
                                                              Все верно, но хотелось бы дополнить: разработчик также не должен ломать то что уже(!) написано и это главная цель всех этих подходов.
                                                              • 0

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

                                                              • 0
                                                                Статья отличная, но как оказалось лучше не упоминать SOLID всуе. Применяю данные принципы довольно давно. Это помогает также выстроить правильный флоу разработки, декомпозиции задач и сделать ревью пул реквестов более продуктивными. А также оптимизировать производительность интерфейсов без рефакторинга, заворачивая боле мелкие компоненты в собственные контейнеры.
                                                                • 0

                                                                  Круто, рад это слышать от автора того самого доклада!

                                                                • 0
                                                                  Будьте добры, напишите полноценный компонент формы для создания таблиц в БД по описанному принципу, которая имела бы следующие характеристики:
                                                                  1. Два инпута: дроплист и поле ввода;
                                                                  2. В дроплист собираются и выводятся названия подсайтов, для которых подгружаются существующие на них таблицы (таблицы не отображаются);
                                                                  3. В поле ввода требуется ввести название новой таблицы. Если в выбранном в дроплисте подсайте такая таблица есть, то зафейлить валидацию и пометить поле как невалидное.
                                                                  4. При изменении дроплиста, при существующем значении в поле ввода, опять валидировать это поле и соответственно реагировать: фейлить или саксессить.

                                                                  Хочется посмотреть SOLID в действии. Благодарю
                                                                  • 0

                                                                    Если это компонент приложения, то есть он существует в единственном виде (как похоже из описания), то заморачиваться с SOLID тут нет смысла. SOLID нужен для компонентов, которые используются в разных контекстах, меняя свой вид в зависимости от конкретной задачи.

                                                                    • 0
                                                                      А вообще можно хоть что-то рабочее посмотреть на SOLID?
                                                                        • 0
                                                                          не, теории с меня хватит, мне нужны проекты, где можно потыкать мышкой и покурить исходники. Кстати, ts как по мне шаг назад. Нам дали свободу с нестрогой типизацией, а мы вместо того, чтобы ей пользоваться и наслаждаться опять лезем в кандалы строгой типизации. Хотя кто к чему привык.
                                                                          • 0

                                                                            Свобода вообще без типизации была в ассемблере :)

                                                                            • 0

                                                                              Жизнь дала нам свободу переломать себе ноги, а мы, вместо того, чтобы ей пользоваться и наслаждаться, ставим перила у лестниц.

                                                                              • 0
                                                                                ага а потом придумывать (int)string, чтобы удобнее эту строгость обойти.
                                                                                • 0

                                                                                  Нет, parseInt(string,10) и строгость тут как раз помогает.

                                                                                  • 0
                                                                                    тогда зачем явное приведение типов в языках со строгой типизацией раз это так удобно?