Pull to refresh
0
Ястребков Артем@Nerlin

Full-Stack Developer

Send message
В которых разобраться в разы проще по описанным выше причинам. Если нужен минимальный функционал, то не нужно будет изучать весь ваш большой класс Form, а достаточно просто ознакомиться с несколькими простыми интерфейсами.
Думаю да, а как же еще. Это вопросы скорее к JetBrains. Понятное дело, что с TypeScript все это проще, я никак не приуменьшаю его возможностей, мне даже он очень нравится. Без него в более сложной ситуации придется разбирать Refactoring Preview и удалять оттуда все то, что на самом деле рефакторить нам сейчас не надо, и нельзя просто сделать вот так:

Переименование
image
И да, ссылки найти сможет и даже переименовать (на скриншоте рефакторинг переименования name в классе Data в value):
image

Просто конечно не все так тривиально и в сложных проектах нужно быть внимательнее, что именно он находит, всегда есть риск переименовать что-то ненужное. Но в основном, если код написан без неявного поведения в виде примешивания атрибутов в неизвестном контексте, то IDE вас поймет.
Естественно будет искать все, где есть использование name. Понятное дело, что некоторые вещи намного проще с TypeScript, иначе бы его не разрабатывали. Но autocomplete, find usages и какие-то рефакторинги не та вещь, ради которой я бы тянул TypeScript в свой проект.
Согласен, однако, для избежания непредвиденных изменений есть ряд и других практик изолирования изменений. В случае добавления нового свойства к компоненту можно добавить также базовое значение, чтобы поддержать существующий код. В случае не JS можно создать перегрузку метода. Тут главное помнить о SRP и не начать пихать в компонент все, что не попадя.
Это у меня в проектах есть и без TypeScript, просто используется WebStorm, он очень хорошо во все это умеет.
Мне обертки нравятся только потому, что они позволяют отвлечься от сложных деталей реализации и позволяют инкапсулировать в одном месте логику компоновки компонентов — если что-то нужно добавить или изменить так, чтобы это повлияло и на другие места приложения, то достаточно сделать это в одном месте. Полный набор компонентов такого делать не позволит, придется бегать по всей системе с Find Usages. Также придется всегда помнить о том, какие компоненты есть в иерархии, но эта проблема скорее больше упирается в спеки и доки проекта, с которыми каждому разработчику, работающему с требованием, придется знакомиться в любом случае.

В случае создания нескольких оберток вместо одного компонента с определенными параметрами, логика начинает «расплываться» на несколько компонентов и мы теряем возможность легко вносить изменения в одно место приложения, т.к. как раз от этого и пытаемся отказаться в данной архитектуре. Т.е. получается, стремясь поддержать SOLID, мы забываем о DRY и YAGNI. Этим мне видится минус данного решения.
TypeScript скорее не о строгой типизации, а о более удобном описании интерфейса классов и сигнатур методов, а, как следствие, и более удобном сопровождении. Намного проще разбираться с кодом, просматривая что принимает на вход метод или какие интерфейсы реализует класс, чем лезть разбираться с реализацией и тратить свое время. Также TypeScript добавляет много возможностей, которых раньше не было в стандарте ES. Если нужна гибкость с типами, используйте UnionTypes или any, никто Вас в кандалы не связывает, просто используйте инструменты с умом.
А если форма используется для комментирования разных сущностей в разных компонентах-контейнерах, например, комментирование видео, фотографий, новостей, каких-нибудь прочих записей? Структура комментария одинаковая, но, допустим, надписи разные в каждом из случаев. Будете использовать общий CommentForm или все же выделите класс для каждого из вариантов использования?
Правильно ли я понял Вашу мысль — есть у нас задача с созданием, редактированием и ответом на комментарии в списке. В зависимости от типа действия нужно изменить заголовок формы и надпись на кнопке — при создании в заголовке написать «Создание комментария» и на кнопке написать «Добавить», и также для редактирования и ответа на комментарий. У нас в системе есть иерархия компонентов для работы с формами. Для данной задачи Вы предлагаете создать три класса CommentCreateForm, CommentEditForm и CommentReplyForm, каждый будет заполнять верно заполнять заголовки и кнопки, правильно я понимаю? Просто пытаюсь понять Вашу точку зрения.
В примере из доков нарушен принцип D, и поэтому этот пример вообще никак не связан со статьей (довольно распространненный паттерн, кстати). Там UserInfo сам принимает решение о рендеринге Avatar, поэтому вызывающий код не может подменить Avatar на что-либо еще, не изменив UserInfo.

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

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

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


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

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

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

Все это конечно актуально, пока мы говорим о presentational components, без какой-то внутренней сложной логики. Как Вы работаете с состоянием приложения и контейнерами тоже было бы интересно почитать.
Кстати, при росте этой иерархии будет тяжелее писать компоненты, основывающиеся на элементах 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, который используется и в других местах, где может быть не нужно скроллить до формы, так что по логике из статьи может быть и стоит задуматься создать еще один класс-декоратор, добавляющий эту логику.
Составные повторно используемые компоненты не должны изменяться, также как и простые. Если что-то не устраивает, то просто заменяешь его. Код составного компонента тривиален, ничего не стоит его заменить.

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

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

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

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


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

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

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

<Modal.YesNoDialog ... />


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

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

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

То есть получается нагородили огород, поговорили о SOLID, но лишь время покажет насколько все это было правильным решением и зачем все это, а может и вообще проще было все-таки делать через props.
Как по мне, вышло слишком сложно. Ради того, чтобы «не заходить за поля тетради», мы реализуем приведенные «спагетти» для простого вызова модального окна. Как показывает опыт, главной причиной ошибок является сложность интерфейса или реализации кода. В данном случае программисту, использующему данный код, нужно помнить обо всём, что нужно поместить внутрь этого гиганта, чтобы вызвать модальное окно, из-за чего значительно увеличивается вероятность ошибки. Стоит ли оно того?
В таком случае у Вас все равно придется написать код, который будет сопоставлять Вашу вторую иерархию отрисовки с основной иерархией, что приводит к той же проблеме.
2

Information

Rating
Does not participate
Location
Орел, Орловская обл., Россия
Date of birth
Registered
Activity