Использование принципа единой ответственности для создания лучших приложений

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

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

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

… Но не следует. Фактически, большинство ваших компонентов, вероятно, слишком велики - или, скорее, делают слишком много.

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



Совет. Используйте 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
  }
}

Наш компонент стал немного сложнее, но все же его легко понять.

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

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

Наш главный компонент проходит этот тест. Так в чем проблема?

Вот другой способ сформулировать тот же принцип:

У [компонента] должна быть только одна причина для изменения.

Это определение дано Робертом Мартином в его книге Гибкая разработка программного обеспечения, и оно имеет большое значение.

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

Чтобы проиллюстрировать почему, давайте усложним наш компонент.

Осложнения

Предположим, что через месяц после того, как мы впервые внедрили компонент 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 на более мелкие компоненты. Но как это разделить?

Начнем сверху. Мы сохраним ответственность за "рендеринг основного представления". Но мы сократим его буквально до рендеринга соответствующих компонентов:

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 несет полную ответственность за визуализацию компонентов макета (боковая панель / заголовок). Мы сопротивляемся искушению возложить на него ответственность за определение того, открыта ли боковая панель.

Вместо этого мы извлекаем это в 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 изменяется только в том случае, если мы меняем способ рендеринга сообщения.

Заключение

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

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

Мы знаем, что наши компоненты со временем будут расти и меняться, но использование этого практического правила предотвратит технический долг и повысит скорость работы команды. Как разделить компоненты - решать вам, но помните: одна причина для изменений.

Спасибо за чтение, не стесняйтесь комментировать!

Учить больше