- PVSM.RU - https://www.pvsm.ru -
Представляем вашему вниманию перевод статьи Scott Domes, которая была опубликована на blog.bitsrc.io. Узнайте под катом, почему компоненты должны быть как можно меньше и как принцип единственной ответственности влияет на качество приложений.

Фото Austin [1] Kirk с Unsplash [2]
Преимущество системы компонентов React (и подобных библиотек) заключается в том, что ваш UI делится на небольшие части, которые легко воспринимаются и могут многократно использоваться.
Эти компоненты компактны (100–200 строк), что позволяет другим разработчикам легко их понимать и видоизменять.
Хотя компоненты, как правило, стараются делать короче, четкого, строгого ограничения их длины нет. React не станет возражать, если вы решите уместить ваше приложение в один пугающе огромный компонент, состоящий из 3 000 строк.
…но делать этого не стоит. Большинство ваших компонентов, скорее всего, и так слишком объемны — а вернее сказать, они выполняют слишком много функций.
В этой статье я докажу, что большинство компонентов (даже с привычной нам длиной в 200 строк) должны быть более узконаправленными. Они должны выполнять только одну функцию, и выполнять ее хорошо. Об этом замечательно рассказывает Эдди Османи вот тут [3].
Совет: когда работаете в JS, применяйте Bit [4], чтобы организовывать, собирать и заново использовать компоненты, как детали лего. Bit — крайне эффективный инструмент для этого дела, он поможет вам и вашей команде сэкономить время и ускорить сборку. Просто попробуйте.
Давайте продемонстрируем, как именно при создании компонентов что-то может пойти не так.
Представим, что у нас есть стандартное приложение для блогеров. И вот что на главном экране:
class Main extends React.Component {
render() {
return (
<div>
<header>
// Header JSX
</header>
<aside id="header">
// Sidebar JSX
</aside>
<div id="post-container">
{this.state.posts.map(post => {
return (
<div className="post">
// Post JSX
</div>
);
})}
</div>
</div>
);
}
}
(Данный пример, как и многие последующие, следует рассматривать как псевдокод.)
Здесь отражена верхняя панель, боковая панель и список постов. Все просто.
Поскольку нам также необходимо загружать посты, мы можем заняться этим, пока компонент монтируется:
class Main extends React.Component {
state = { posts: [] };
componentDidMount() {
this.loadPosts();
}
loadPosts() {
// Load posts and save to state
}
render() {
// Render code
}
}
У нас также есть некая логика, по которой вызывается боковая панель. Если пользователь кликает на кнопку в верхней панели, выезжает боковая. Закрыть ее можно как из верхней, так и из собственно боковой панели.
class Main extends React.Component {
state = { posts: [], isSidebarOpen: false };
componentDidMount() {
this.loadPosts();
}
loadPosts() {
// Load posts and save to state
}
handleOpenSidebar() {
// Open sidebar by changing state
}
handleCloseSidebar() {
// Close sidebar by changing state
}
render() {
// Render code
}
}
Наш компонент стал немного сложнее, но воспринимать его по-прежнему легко.
Можно утверждать, что все его части служат одной цели: отображению главной страницы приложения. Значит, мы следуем принципу единственной ответственности.
Принцип единственной ответственности гласит, что один компонент должен выполнять только одну функцию. Если переформулировать определение, взятое из wikipedia.org [5], то получается, что каждый компонент должен отвечать только за одну часть функционала [приложения].
Наш компонент Main соответствует этому требованию. В чем же проблема?
Перед вами другая формулировка принципа: у любого [компонента] должна быть только одна причина для изменения.
Это определение взято из книги Роберта Мартина «Быстрая разработка программ. Принципы, примеры, практика» [6], и оно имеет большое значение.
Сфокусировавшись на одной причине для изменения наших компонентов, мы сможем создавать более качественные приложения, которые, к тому же, будет легко настраивать.
Для наглядности — давайте усложним наш компонент.
Предположим, что спустя месяц после того, как компонент Main был внедрен, разработчику из нашей команды поручили новую фичу. Теперь пользователь сможет скрывать какой-либо пост (например, если в нем содержится неприемлемый контент).
Это нетрудно сделать!
class Main extends React.Component {
state = { posts: [], isSidebarOpen: false, postsToHide: [] };
// older methods
get filteredPosts() {
// Return posts in state, without the postsToHide
}
render() {
return (
<div>
<header>
// Header JSX
</header>
<aside id="header">
// Sidebar JSX
</aside>
<div id="post-container">
{this.filteredPosts.map(post => {
return (
<div className="post">
// Post JSX
</div>
);
})}
</div>
</div>
);
}
}
Наша коллега легко с этим справилась. Она лишь добавила один новый метод и одно новое свойство. Ни у кого из тех, кто просматривал краткий перечень изменений, возражений не возникло.
Пару недель спустя анонсируется другая фича — усовершенствованная боковая панель для мобильной версии. Вместо того чтобы возиться с CSS, разработчик решает создать несколько компонентов JSX, которые будут запускаться только на мобильных устройствах.
class Main extends React.Component {
state = {
posts: [],
isSidebarOpen: false,
postsToHide: [],
isMobileSidebarOpen: false
};
// older methods
handleOpenSidebar() {
if (this.isMobile()) {
this.openMobileSidebar();
} else {
this.openSidebar();
}
}
openSidebar() {
// Open regular sidebar
}
openMobileSidebar() {
// Open mobile sidebar
}
isMobile() {
// Check if mobile device
}
render() {
// Render method
}
}
Еще одно небольшое изменение. Пара новых удачно названных методов и новое свойство.
И тут у нас возникает проблема. Main по-прежнему выполняет лишь одну функцию (рендеринг главного экрана), но вы посмотрите на все эти методы, с которыми мы теперь имеем дело:
class Main extends React.Component {
state = {
posts: [],
isSidebarOpen: false,
postsToHide: [],
isMobileSidebarOpen: false
};
componentDidMount() {
this.loadPosts();
}
loadPosts() {
// Load posts and save to state
}
handleOpenSidebar() {
// Check if mobile then open relevant sidebar
}
handleCloseSidebar() {
// Close both sidebars
}
openSidebar() {
// Open regular sidebar
}
openMobileSidebar() {
// Open mobile sidebar
}
isMobile() {
// Check if mobile device
}
get filteredPosts() {
// Return posts in state, without the postsToHide
}
render() {
// Render method
}
}
Наш компонент становится большим и громоздким, его сложно понять. И с расширением функционала ситуация будет только усугубляться.
Что же пошло не так?
Давайте вернемся к определению принципа единственной ответственности: у любого компонента должна быть лишь одна причина для изменения.
Ранее мы изменили способ отображения постов, поэтому пришлось изменить и наш компонент Main. Далее мы изменили способ открытия боковой панели — и вновь изменили компонент Main.
У этого компонента множество не связанных между собой причин для изменения. Это означает, что он выполняет слишком много функций.
Другими словами, если вы можете в значительной степени изменить часть вашего компонента и это не приведет к изменениям в другой его части, значит, у вашего компонента слишком много ответственности.
Решение проблемы простое: необходимо разделить компонент Main на несколько частей. Как это сделать?
Начнем сначала. Рендеринг главного экрана остается ответственностью компонента Main, но мы сокращаем ее только до отображения связанных компонентов:
class Main extends React.Component {
render() {
return (
<Layout>
<PostList />
</Layout>
);
}
}
Замечательно.
Если мы вдруг изменим способ компоновки главного экрана (например, добавим дополнительные разделы), то изменится и Main. В остальных случаях у нас не будет причин его трогать. Прекрасно.
Давайте перейдем к Layout:
class Layout extends React.Component {
render() {
return (
<SidebarDisplay>
{(isSidebarOpen, toggleSidebar) => (
<div>
<Header openSidebar={toggleSidebar} />
<Sidebar isOpen={isSidebarOpen} close={toggleSidebar} />
</div>
)}
</SidebarDisplay>
);
}
}
Тут немного сложнее. На Layout лежит ответственность за рендеринг компонентов разметки (боковая панель / верхняя панель). Но мы не поддадимся соблазну и не наделим Layout ответственностью определять, открыта боковая панель или нет.
Мы назначаем эту функцию компоненту SidebarDisplay, который передает необходимые методы или состояние компонентам Header и Sidebar.
(Выше представлен пример паттерна Render Props via Children [7] в React. Если вы не знакомы с ним, не переживайте. Тут важно существование отдельного компонента, управляющего состоянием «открыто/закрыто» боковой панели.)
И потом, сам Sidebar может быть довольно простым, если отвечает только за рендеринг боковой панели справа.
class Sidebar extends React.Component {
isMobile() {
// Check if mobile
}
render() {
if (this.isMobile()) {
return <MobileSidebar />;
} else {
return <DesktopSidebar />;
}
}
}
Мы снова противимся соблазну вставить JSX для компьютеров / мобильных устройств прямо в этот компонент, ведь в таком случае у него будет две причины для изменения.
Посмотрим на еще один компонент:
class PostList extends React.Component {
state = { postsToHide: [] }
filterPosts(posts) {
// Show posts, minus hidden ones
}
hidePost(post) {
// Save hidden post to state
}
render() {
return (
<PostLoader>
{
posts => this.filterPosts(posts).map(post => <Post />)
}
</PostLoader>
)
}
}
PostList меняется, только если мы меняем способ отрисовки списка постов. Кажется очевидным, не так ли? Как раз это нам и нужно.
PostLoader меняется, только если мы изменяем способ загрузки постов. И наконец, Post меняется, только если мы меняем способ отрисовки поста.
Все эти компоненты крошечные и выполняют одну маленькую функцию. Причины изменений в них легко выявить, а сами компоненты — протестировать и исправить.
Теперь наше приложение гораздо проще модифицировать — переставлять компоненты, добавлять новый и расширять имеющийся функционал. Вам достаточно лишь взглянуть на какой-либо файл компонента, чтобы определить, для чего он.
Мы знаем, что наши компоненты будут изменяться и расти с течением времени, но применение этого универсального правила поможет вам избежать технического долга и увеличить скорость работы команды. Как распределять компоненты, решать вам, но помните — для изменения компонента должна быть только одна причина.
Спасибо за внимание, и ждем ваших комментариев!
Автор: Plarium
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ui/306502
Ссылки в тексте:
[1] Austin: https://unsplash.com/photos/QZenflkkwt0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText
[2] Unsplash: https://unsplash.com/search/photos/puppy?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText
[3] тут: https://addyosmani.com/first/
[4] применяйте Bit: https://github.com/teambit/bit
[5] wikipedia.org: https://en.wikipedia.org/wiki/Single_responsibility_principle
[6] «Быстрая разработка программ. Принципы, примеры, практика»: https://books.google.com/books?id=0HYhAQAAIAAJ&redir_esc=y
[7] Render Props via Children: https://reactjs.org/docs/render-props.html
[8] Источник: https://habr.com/ru/post/437504/?utm_campaign=437504
Нажмите здесь для печати.