При разработке любого, даже простого веб-приложения возникает необходимость повторного использования кода.
В разных местах сайта рано или поздно обнаруживаются похожие участки разметки и логики, дублировать которые совсем не хочется.
Однако при решении этой задачи очень легко наступить на грабли и сделать все очень плохо.
?Эта статья во многом вдохновлена
"> Доклад Павла Силина на РИТ 2017
, однако здесь много моего собственного опыта и мыслей.
Примеры будут на React + TypeScript, но подход не привязан к какой-либо технологии.
" alt="Замени и властвуй: НАДЕЖНЫЙ подход к разработке повторно используемых компонентов в Интернете.
">
Чего не делать
Когда вы сталкиваетесь с ситуацией дублирования кода, естественным желанием является вынести этот код в отдельный компонент и использовать его там, где это необходимо.В качестве примера возьмем модальное окно.
Казалось бы, что может быть проще – пошли и сделали:
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>;
Нетрудно догадаться, что на этом история не закончилась.
Позже стало необходимо показывать окна без «крестика» в правом верхнем углу, без заголовка или с другим заголовком, с разными отступами от краев окна, некоторые окна приходилось закрывать нажатием снаружи, а некоторые нет. Неизбежно растущее количество вариантов компонента «пахнет», но с этим как-то можно смириться.
Что действительно пугает, так это то, что каждый необычный вариант использования компонента вынуждал нас менять компонент, который использовался во многих других местах.
При добавлении каждой опции мы редактировали код, редактировали макет, и это теоретически могло нарушить логику где-то еще, где используется такое же модальное окно.
То есть добавление новых функций грозит вызвать регрессии в самых неожиданных местах.
В моем примере была функция, но это могло быть что угодно — компонент реагирования с огромными реквизитами, плагин jquery со множеством опций, базовый класс с кучей наследников и переопределенных методов, помощник ASP.NET Razor со множеством параметров.
scss миксин и т.д. Наступить на эти грабли можно в любой технологии и в самых разных формах.
Заменить и победить
Решение этой проблемы придумали римляне — разделяй и властвуй, а принципы SOLID сформулировал Роберт Мартин ещё в 2000-х.И несмотря на то, что SOLID больше относится к объектно-ориентированной архитектуре, а React — больше к функциональной парадигме, все эти принципы можно и нужно применять при проектировании повторно используемых React-компонентов.
Однако важно соблюдать их все сразу, а не по отдельности.
Скажем так, недостаточно просто делать «маленькие компоненты».
Это только первая буква S, а без всех остальных ничего не получится.
Пройдемся по всем буквам:
- С (единая ответственность) — сделать многоразовые компоненты очень маленькими и простыми, с минимумом ответственности;
- О (открыто-закрыто) — мы никогда и ни при каких обстоятельствах не модифицируем код часто используемых компонентов;
- л (подмена Лискова) – любой компонент можно заменить другим так, чтобы все остальные компоненты не заметили подмены;
- я (разделение интерфейсов) — вместо написания «обобщённых» компонентов на все случаи жизни мы пишем простые конкретные реализации;
- Д (инверсия зависимостей) — решение о том, какой компонент будет использоваться в каждом случае, должно приниматься вызывающим кодом.
Мы пишем простые (удручающе простые) компоненты, которые соединяются друг с другом, как детали LEGO. Ни одна деталь ничего не знает об остальных.
Когда нам нужно сделать конкретную вещь, мы берем эту дизайнерскую коробку и собираем именно то, что нам нужно.
Если какая-то деталь нас не устроит, мы легко можем ее выбросить и взять другую (например, сделать свою).
Это очень просто, ведь каждая из деталей сама по себе тривиальна, и ничего не стоит сделать другую, похожую, но подходящую именно для этого случая.
Итак, вместо изменять существующие компоненты, мы просто заменять их, благодаря чему мы даже теоретически не можем сломать что-то в другом месте приложения.
Ключевым моментом здесь является то, что вместо того, чтобы менять компонент, который используется во многих местах, мы просто заменяем его другим.
Это важно для компонентов в сети, поскольку любое изменение стилей может привести к нарушению макета в некоторых случаях использования компонента.
Единственный надежный способ защититься от этого — не менять однажды написанные компоненты (если они используются повторно).
Давайте проведем рефакторинг нашего модального окна в соответствии с этими принципами.
Нам нужно сделать модальное окно примерно таким:
" alt="Замени и властвуй: НАДЕЖНЫЙ подход к разработке повторно используемых компонентов в Интернете.
">
Как научил нас горький опыт, в этом окне может измениться все что угодно.
Начиная от кнопок и заканчивая отступами контента.
Это связано с тем, что модальное окно используется во многих местах.
Особое внимание следует уделить проектированию таких часто используемых компонентов.
Вот какой набор компонентов у меня получился:
- Позиционирование окна – помещает что-то в центр экрана;
- Тусклый фон — создает полупрозрачный div на весь экран;
- Рамка окна – определяет размеры и заполнение внутри окна;
- Поле заголовка — добавляет отступы для заголовка и рисует разделительную линию;
- Заголовок – предшествует стилизации текста заголовка (в основном размеру шрифта);
- Кнопка закрытия (крестик);
- Поле содержимого — добавляет отступы для содержимого окна;
- Диалоговое окно с кнопками — добавляет отступы и размещает кнопки справа;
- Кнопка представляет собой обычную кнопку, никак не связанную с диалогом.
<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), чем добавить какие-то варианты (О).
Стоит отметить, что идеал, конечно, недостижим.
Например, ModalDialogBox определяет размеры и заливку.
То есть у него как бы есть две обязанности.
На такие компромиссы приходится идти, чтобы избежать излишнего многословия.
Однако это не так уж и страшно, поскольку в будущем мы всегда можем заменить этот компонент двумя другими — отдельными компонентами по размерам и заполнению, если возникнет такая необходимость.
Прелесть этого подхода в том, что он прощает ошибки проектирования.
Вы всегда сможете их исправить позже, добавить дополнительную гибкость, не нарушая ранее написанный код. Если такой уровень гибкости нужен редко, то для удобства использования в стандартных случаях можно сделать компоненты-обертки, которые просто объединяют некоторые из этих небольших компонентов.
Вариантов у них будет много, но это допустимо — мы всегда можем заменить эти обертки на другие, либо использовать напрямую оригинальные компоненты.
Например, мы можем сделать следующую обертку: <CommonModalDialog header="Header text"
isOpen={this.state.open} onClose={this.onClose}>
Modal content
</CommonModalDialog>
Важно понимать, что сложные многоразовые компоненты не следует менять, как и простые.
Любое изменение может привести к поломке макета в том или ином конкретном случае, поэтому если что-то в компоненте не подходит, то нужно просто заменить его на другое.
Код-обертка будет представлять собой простую композицию существующих компонентов (всего вышеперечисленного), поэтому заменить их не составит труда.
Обратно в реальность
В идеальном мире мы бы повсюду писали всё SOLID, носили белую одежду и махали крыльями от счастья.К сожалению, в действительности применение такого подхода не везде оправдано, а местами даже вредно.
Вот его основные недостатки:
- Дорогой.
Проектирование и разработка всех этих небольших компонентов требует много времени и усилий.
Вам не только придется писать много сервисного кода, документации и тестов, но и спроектировать эти компоненты таким образом, чтобы они ничего друг о друге не знали, но при этом корректно взаимодействовали друг с другом.
.
Это очень сложно и с точки зрения бизнеса стоит больших денег (время разработчика — деньги).
- Разделение сущностей.
В приведенном выше примере вместо одного модального окна мы получили 9 крошечных компонентов.
Соответственно, логика работы окна оказалась размазана по всем этим компонентам.
В данном случае это не критично, т.к.
в окне нет особой логики, но это может иметь серьёзные последствия для компонентов приложения.
" alt="Замени и властвуй: НАДЕЖНЫЙ подход к разработке повторно используемых компонентов в Интернете.
">
Можно начать разбивать его на кучу независимых небольших компонентов, там будет отдельная иконка пользователя, отдельное имя, отдельное меню.
Мы потратим немало усилий на организацию взаимодействия между этими независимыми компонентами.
Но что мы получим в итоге? Это меню существует в единственном числе и только в таком виде.
В этом меню есть некоторая логика — своя единая модель данных (информация о пользователе), определенный состав меню (набор действий), поведение (нажатие открывает меню).
Все это логика конкретного приложения, которая определяется бизнес-задачами и диктуется предметной областью.
Распространяя эту логику на множество мест, мы не только создаем себе лишние трудности, но и усложняем поддержку и обслуживание нашего сайта.
Другому программисту будет сложно найти место, где привязывается обработчик к событию клика, открывающему меню, потому что он будет (конечно согласно SOLID) спрятан где-то в недрах нашей архитектуры.
Отсюда следует, что необходимо четко разделить многоразовые компоненты И компоненты приложения .
Первые максимально абстрактны, просты и гибки, а вторые используют первые, но при этом максимально целостны и понятны.
Размер компонентов приложения должен быть ограничен исходя из концептуальной декомпозиции сайта на логические блоки и очевидных соображений, чтобы размер файла не становился слишком огромным.
Чтобы бизнес-компоненты не превратились в монстров, нужно извлечь из них как можно больше всего, что может стать многоразовым компонентом.
То есть бизнес-составляющая в идеале должна работать преимущественно с деталями LEGO, составляя из них сайт определенного типа.
Скажем, в нашем примере мы можем создать группу повторно используемых компонентов для описания меню, рисования округленного значка, кнопки раскрытия меню и т. д. Все эти компоненты никак не будут связаны с пользовательским меню.
Их даже можно было бы поместить в отдельный пакет npm и использовать в других проектах.
Они будут простыми и гибкими и будут соответствовать всем принципам SOLID. Однако сам компонент, отрисовывающий пользовательское меню, остался бы хоть и не совсем маленьким, но цельным и понятным.
Вся связанная с ним логика приложения будет описана в одном файле, и ее будет очень легко понять.
Заключение
При разработке многоразовых компонентов важно соблюдать все принципы SOLID. Это позволит вам беспрепятственно использовать эти компоненты в самых разных контекстах, адаптировать их к самым невообразимым ситуациям и не бояться, что добавление нового функционала на сайт сломает уже существующий.Правильное применение этих принципов устраняет ошибки проектирования и позволяет найти компромисс между гибкостью и многословием, не жертвуя при этом расширяемостью.
Также важно разделить компоненты на повторно используемые компоненты и компоненты приложения.
Последний лучше написать относительно большим и без лишней гибкости, но цельным и простым в обслуживании.
Повторное использование таких компонентов должно быть минимальным, а если возникнет такая необходимость, то их следует перевести в категорию многоразовых со всеми вытекающими.
Такой подход позволяет, с одной стороны, следовать DRY, но, с другой стороны, сохранить код понятным и простым в поддержке и сопровождении.
Теги: #react #JavaScript #CSS #HTML #архитектура приложения #дизайн #solid #dry #Components #web #разработка веб-сайтов #JavaScript #HTML #react.js
-
Лазерный Принтер Dell 1710N
19 Dec, 24 -
Оптимизация Сайта. Диагнозы И Курсы Лечения
19 Dec, 24 -
Книга Без Названия
19 Dec, 24 -
Пол Грэм: Что Я Узнал Из Hacker News
19 Dec, 24