Крошечные компоненты: что может пойти не так? Используем принцип единственной ответственности

в 11:30, , рубрики: ES6, front-end development, fronted development, javascript, React, ReactJS, software development, UI, Блог компании Plarium, компоненты, приложения, Программирование, разработка мобильных приложений

Представляем вашему вниманию перевод статьи Scott Domes, которая была опубликована на blog.bitsrc.io. Узнайте под катом, почему компоненты должны быть как можно меньше и как принцип единственной ответственности влияет на качество приложений.

Крошечные компоненты: что может пойти не так? Используем принцип единственной ответственности - 1
Фото Austin Kirk с Unsplash

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

Эти компоненты компактны (100–200 строк), что позволяет другим разработчикам легко их понимать и видоизменять.

Хотя компоненты, как правило, стараются делать короче, четкого, строгого ограничения их длины нет. React не станет возражать, если вы решите уместить ваше приложение в один пугающе огромный компонент, состоящий из 3 000 строк.

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

В этой статье я докажу, что большинство компонентов (даже с привычной нам длиной в 200 строк) должны быть более узконаправленными. Они должны выполнять только одну функцию, и выполнять ее хорошо. Об этом замечательно рассказывает Эдди Османи вот тут.

Совет: когда работаете в JS, применяйте Bit, чтобы организовывать, собирать и заново использовать компоненты, как детали лего. 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, то получается, что каждый компонент должен отвечать только за одну часть функционала [приложения].

Наш компонент Main соответствует этому требованию. В чем же проблема?

Перед вами другая формулировка принципа: у любого [компонента] должна быть только одна причина для изменения.

Это определение взято из книги Роберта Мартина «Быстрая разработка программ. Принципы, примеры, практика», и оно имеет большое значение.

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

Для наглядности — давайте усложним наш компонент.

Усложнение

Предположим, что спустя месяц после того, как компонент 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 в 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js