Перевод: Лю Сяоси
Оригинальная ссылка:Рисовое путешествие avlutin.com/7-architect…
длина оригиналаОченьОно длинное, но содержание слишком привлекательно для меня, поэтому я не могу не перевести его. Эта статья очень полезна для написания повторно используемых и поддерживаемых компонентов React. Но так как место слишком длинное, я должен разделить его, эта статья посвященаSRP
, Принцип единой ответственности.
Другие статьи можно нажать: GitHub.com/Иветт Л.А. Ю/Б…
Я был разделительной линией -------------------------
Мне нравится компонентный подход React к разработке. Вы можете разделить сложные пользовательские интерфейсы на отдельные компоненты, воспользовавшись возможностью повторного использования компонентов и абстрагировавшись от манипуляций с DOM.
Компонентная разработка эффективна: сложная система строится из специализированных, простых в управлении компонентов. Однако только хорошо спроектированные компоненты могут обеспечить преимущества композиции и повторного использования.
Несмотря на сложность приложения, вы должны постоянно ходить по тонкой грани архитектурной правильности, чтобы уложиться в сроки и внезапные изменения. Вы должны разделить компоненты, чтобы сосредоточиться на одной задаче и быть хорошо протестированы.
К сожалению, всегда проще пойти по ложному пути: написать большие компоненты с множеством обязанностей, тесно связать компоненты, забыть о модульных тестах. Это увеличивает технический долг, что затрудняет изменение существующих функций или создание новых.
При написании приложений React я часто спрашиваю себя:
- Как правильно структурировать компоненты?
- Когда следует разделить большой компонент на более мелкие компоненты?
- Как спроектировать, чтобы предотвратить связь между сильно связанными компонентами?
К счастью, надежные компоненты имеют общие характеристики. Давайте рассмотрим эти 7 полезных критериев (эта статья посвящена толькоSRP
, остальные рекомендации находятся в процессе) и детализировать его в тематическом исследовании.
единственная ответственность
Когда у компонента есть только одна причина для изменения, он несет единственную ответственность.
Фундаментальным принципом, который следует учитывать при написании компонентов React, является принцип единой ответственности. Принцип единой ответственности (аббревиатура:SRP
) требует, чтобы у компонента была одна и только одна причина для изменения.
Ответственность компонента может состоять в том, чтобы отображать список, или отображать средство выбора даты, или генерироватьHTTP
запросить, или нарисовать диаграмму, или лениво загрузить изображение и т. д. Ваш компонент должен выбрать только одну обязанность и реализовать ее. Когда вы изменяете способ, которым компонент реализует свои обязанности (например, изменяете ограничение на количество отображаемых списков), у него есть причина для изменения.
Почему важно иметь только одну причину для изменений? Потому что таким образом модификация компонентов изолирована и контролируется. Принцип единой ответственности распределяет компоненты таким образом, чтобы они фокусировались на чем-то одном. Компоненты, ориентированные на одну задачу, легко кодировать, модифицировать, повторно использовать и тестировать.
Возьмем несколько примеров
Пример 1: Компонент извлекает удаленные данные, и, соответственно, при изменении логики выборки у него есть причина для изменения.
Причина изменения:
- Изменить URL-адрес сервера
- Изменить формат ответа
- Использование других библиотек HTTP-запросов
- Или просто любая модификация, связанная с логикой выборки.
Пример 2. Табличный компонент сопоставляет массив данных со списком компонентов-строк, поэтому есть причина, которую необходимо изменить при изменении логики сопоставления.
Причина изменения:
- Вам необходимо ограничить количество визуализируемых компонентов строки (например, отображать до 25 строк)
- Когда проект не подлежит отображению, требуется вывести сообщение «список пуст».
- Или просто любая модификация, связанная с отображением массивов на компоненты строк.
У вашего компонента много обязанностей? Если ответ «да», разделите компоненты на части для каждой отдельной обязанности.
Если вы находите SRP немного расплывчатым, читайте дальшеэта статья. Модули, написанные на ранних стадиях проекта, будут часто изменяться, пока не будет достигнута стадия релиза. Эти изменения часто требуют, чтобы компоненты были легко модифицируемыми по отдельности: это также является целью SRP.
1.1 Ловушка множественной ответственности
Когда компонент имеет более одной ответственности, возникает общая проблема. На первый взгляд такой подход кажется безобидным и менее трудоемким:
- Вы сразу начинаете программировать: нет необходимости определять обязанности и соответствующим образом планировать структуру
- Один большой компонент может все: нет необходимости создавать компонент для каждой обязанности.
- Нет разделения - нет накладных расходов: нет необходимости создавать для связи между разделенными компонентами
props
а такжеcallbacks
Эту наивную структуру легко кодировать в начале. Но по мере того, как приложение растет и усложняется, в более поздних модификациях возникают трудности. Компоненты, выполняющие несколько функций одновременно, имеют много причин для изменения. Основная проблема, которая возникает сейчас, заключается в том, что изменение компонента по какой-либо причине непреднамеренно влияет на другие обязанности, которые реализует этот же компонент.
Не выключайте выключатель света, так как он действует и на лифт.
Этот дизайн оченьХрупкий. Непреднамеренные побочные эффекты трудно предсказать и контролировать.
Например,<ChartAndForm>
Есть две обязанности одновременно: рисование диаграммы и обработка формы, предоставляющей данные для этой диаграммы.<ChartandForm>
Причин изменения две: рисование диаграмм и работа с формами.
Когда вы изменяете поля формы (например, помещаете<input>
превратиться в<select>
Когда вы непреднамеренно прервали рендеринг графика. Кроме того, для достижения графика не используется многоразовая, потому что она соединена вместе с деталями формы.
Решение нескольких задач требует<ChartAndForm>
Разделить на две составляющие:<Chart>
а также<Form>
. У каждого компонента есть только одна обязанность: рисование диаграммы или обработка формы. Связь между компонентами осуществляется черезprops
выполнить.
Наихудшим случаем проблемы множественной ответственности является так называемый компонент бога (аналог бога-объекта). Компоненты Бога, как правило, знают и делают все. вы могли видеть, что это называется<Application>
,<Manager>
,<Bigcontainer>
или<Page>
, код превышает 500 строк.
Разбейте компонент бога, сделав его совместимым с SRP с помощью композиции. (Композиция — это способ объединения компонентов для создания более крупных компонентов. Композиция лежит в основе React.)
1.2 Практический пример: возложение на компонент только одной ответственности
Представьте, что компонент выдает на выделенный серверHTTP
запрос на получение текущей погоды. Когда данные успешно получены, компонент использует ответ для отображения информации о погоде:
import axios from 'axios';
// 问题: 一个组件有多个职责
class Weather extends Component {
constructor(props) {
super(props);
this.state = { temperature: 'N/A', windSpeed: 'N/A' };
}
render() {
const { temperature, windSpeed } = this.state;
return (
<div className="weather">
<div>Temperature: {temperature}°C</div>
<div>Wind: {windSpeed}km/h</div>
</div>
);
}
componentDidMount() {
axios.get('http://weather.com/api').then(function (response) {
const { current } = response.data;
this.setState({
temperature: current.temperature,
windSpeed: current.windSpeed
})
});
}
}
Имея дело с похожей ситуацией, спросите себя: нужно ли вам разбивать компоненты на более мелкие? На этот вопрос лучше всего ответить, определив, как компонент может измениться в зависимости от его обязанностей.
Есть две причины изменения этого компонента погоды:
-
componentDidMount()
серединаfetch
Логика: URL-адрес сервера или формат ответа могут измениться. -
render()
Отображение погоды в: то, как компонент отображает погоду, можно менять несколько раз.
Решение состоит в том, чтобы<Weather>
Разделен на два компонента: каждый компонент имеет только одну ответственность. названный<WeatherFetch>
а также<WeatherInfo>
.
<WeatherFetch>
Компонент отвечает за получение погоды, извлечение данных ответа и сохранение их вstate
середина. Единственная причина для его изменения — получить изменения логики данных.
import axios from 'axios';
// 解决措施: 组件只有一个职责就是请求数据
class WeatherFetch extends Component {
constructor(props) {
super(props);
this.state = { temperature: 'N/A', windSpeed: 'N/A' };
}
render() {
const { temperature, windSpeed } = this.state;
return (
<WeatherInfo temperature={temperature} windSpeed={windSpeed} />
);
}
componentDidMount() {
axios.get('http://weather.com/api').then(function (response) {
const { current } = response.data;
this.setState({
temperature: current.temperature,
windSpeed: current.windSpeed
});
});
}
}
Каковы преимущества этой структуры?
Например, вы хотите использоватьasync/await
вместо этого синтаксисpromise
Зайдите на сервер, чтобы получить ответ. Причина изменения: Изменить логику приобретения
// 改变原因: 使用 async/await 语法
class WeatherFetch extends Component {
// ..... //
async componentDidMount() {
const response = await axios.get('http://weather.com/api');
const { current } = response.data;
this.setState({
temperature: current.temperature,
windSpeed: current.windSpeed
});
}
}
потому что<WeatherFetch>
Есть только одна причина для изменения: изменитьfetch
логике, поэтому любая модификация этого компонента изолирована. использоватьasync/await
Напрямую не влияет на отображение погоды.
<WeatherFetch>
оказывать<WeatherInfo>
. Последний отвечает только за отображение погоды, а причиной изменения может быть только изменение визуального отображения.
// 解决方案: 组件只有一个职责,就是显示天气
function WeatherInfo({ temperature, windSpeed }) {
return (
<div className="weather">
<div>Temperature: {temperature}°C</div>
<div>Wind: {windSpeed} km/h</div>
</div>
);
}
давай меняться<WeatherInfo>
, если не отображается“wind:0 km/h”
вместо этого показать“wind:calm”
. Вот почему визуальное отображение погоды меняется:
// 改变原因: 无风时的显示
function WeatherInfo({ temperature, windSpeed }) {
const windInfo = windSpeed === 0 ? 'calm' : `${windSpeed} km/h`;
return (
<div className="weather">
<div>Temperature: {temperature}°C</div>
<div>Wind: {windInfo}</div>
</div>
);
}
Аналогично, да<WeatherInfo>
Модификации изолированы и не повлияют<WeatherFetch>
компоненты.
<WeatherFetch>
а также<WeatherInfo>
есть свои обязанности. Изменение одного компонента мало влияет на другой. Именно здесь вступает в действие принцип единой ответственности: изменение изоляции с минимальным и предсказуемым воздействием на другие компоненты системы.
1.3 Практический пример: HOC предпочитает принцип единой ответственности
Использование комбинации разрозненных компонентов по ответственности не всегда помогает следовать принципу единой ответственности. Другой хорошей практикой являются компоненты более высокого порядка (сокращенноHOC
)
Компонент более высокого порядка — это функция, которая принимает компонент и возвращает новый компонент.
HOC
Обычно используется для добавления новых свойств или изменения существующих значений свойств для инкапсулированных компонентов. Этот метод называется проксированием свойств:
function withNewFunctionality(WrappedComponent) {
return class NewFunctionality extends Component {
render() {
const newProp = 'Value';
const propsProxy = {
...this.props,
// 修改现有属性:
ownProp: this.props.ownProp + ' was modified',
// 增加新属性:
newProp
};
return <WrappedComponent {...propsProxy} />;
}
}
}
const MyNewComponent = withNewFunctionality(MyComponent);
Вы также можете управлять результатом рендеринга, контролируя процесс рендеринга компонента ввода. этоHOC
Этот метод называется захватом рендеринга:
function withModifiedChildren(WrappedComponent) {
return class ModifiedChildren extends WrappedComponent {
render() {
const rootElement = super.render();
const newChildren = [
...rootElement.props.children,
// 插入一个元素
<div>New child</div>
];
return cloneElement(
rootElement,
rootElement.props,
newChildren
);
}
}
}
const MyNewComponent = withModifiedChildren(MyComponent);
Если вы хотите погрузиться в практику HOCS, я рекомендую прочитать «Отзывчивые компоненты высшего порядка в деталях».
Давайте рассмотрим пример, чтобы увидеть, как метод проксирования атрибутов HOC может помочь с разделением обязанностей.
компоненты<PersistentForm>
Зависит отinput
Поля ввода и кнопкиsave to storage
сочинение. После изменения введенного значения нажмитеsave to storage
кнопка для записиlocalStorage
середина.
input
статус вhandlechange(event)
метод обновлен. При нажатии кнопки значение будет сохранено в локальном хранилище, вhandleclick()
Обработка в:
class PersistentForm extends Component {
constructor(props) {
super(props);
this.state = { inputValue: localStorage.getItem('inputValue') };
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
render() {
const { inputValue } = this.state;
return (
<div className="persistent-form">
<input type="text" value={inputValue}
onChange={this.handleChange} />
<button onClick={this.handleClick}>Save to storage</button>
</div>
);
}
handleChange(event) {
this.setState({
inputValue: event.target.value
});
}
handleClick() {
localStorage.setItem('inputValue', this.state.inputValue);
}
}
К сожалению:<PersistentForm>
Имеет 2 обязанности: управляет полями формы, сохраняет только введенные данные.localStorage
.
Давайте рефакторинг<PersistentForm>
компонент, у которого есть только одна обязанность: представление полей формы и присоединение обработчиков событий. Он не должен знать, как использовать хранилище напрямую:
class PersistentForm extends Component {
constructor(props) {
super(props);
this.state = { inputValue: props.initialValue };
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
render() {
const { inputValue } = this.state;
return (
<div className="persistent-form">
<input type="text" value={inputValue}
onChange={this.handleChange} />
<button onClick={this.handleClick}>Save to storage</button>
</div>
);
}
handleChange(event) {
this.setState({
inputValue: event.target.value
});
}
handleClick() {
this.props.saveValue(this.state.inputValue);
}
}
Компоненты получают сохраненные входные значения от инициализаторов свойств и используют функции свойствsaveValue(newValue)
для сохранения введенного значения. Этиprops
с помощью технологии атрибутивного проксиwithpersistence()
Предоставлено ХОК.
Сейчас<PersistentForm>
соответствоватьSRP
. Единственная причина изменения заключается в изменении полей формы.
Ответственность за запросы и сохранение в локальном хранилище возлагается наwithPersistence()
ХОК обязуется:
function withPersistence(storageKey, storage) {
return function (WrappedComponent) {
return class PersistentComponent extends Component {
constructor(props) {
super(props);
this.state = { initialValue: storage.getItem(storageKey) };
}
render() {
return (
<WrappedComponent
initialValue={this.state.initialValue}
saveValue={this.saveValue}
{...this.props}
/>
);
}
saveValue(value) {
storage.setItem(storageKey, value);
}
}
}
}
withPersistence()
ЯвляетсяHOC
, чьи обязанности долговечны. Он не знает никаких подробностей о полях формы. Он фокусируется только на одной работе: предоставлении входящих компонентовinitialValue
строка иsaveValue()
функция.
Буду<PersistentForm>
а такжеwithpersistence()
Используется вместе для создания нового компонента<LocalStoragePersistentForm>
. Он подключен к локальному хранилищу и может использоваться в приложении:
const LocalStoragePersistentForm
= withPersistence('key', localStorage)(PersistentForm);
const instance = <LocalStoragePersistentForm />;
если только<PersistentForm>
использовать правильноinitialValue
а такжеsaveValue()
свойство, любая модификация компонента не может нарушитьwithPersistence()
Логика сохранения в хранилище.
И наоборот: покаwithPersistence()
предоставить правильныйinitialValue
а такжеsaveValue()
,правильноHOC
Любая модификация не должна нарушать способ обработки полей формы.
Эффективность SRP снова демонстрируется: модифицируя изоляцию, тем самым уменьшая влияние на остальную часть системы.
Кроме того, увеличится возможность повторного использования кода. можно поставить любую другую форму<MyOtherForm>
Подключиться к локальному хранилищу:
const LocalStorageMyOtherForm
= withPersistence('key', localStorage)(MyOtherForm);
const instance = <LocalStorageMyOtherForm />;
Вы можете легко изменить тип хранилища наsession storage
:
const SessionStoragePersistentForm
= withPersistence('key', sessionStorage)(PersistentForm);
const instance = <SessionStoragePersistentForm />;
Первоначальный вариант<PersistentForm>
Нет никаких преимуществ модификации изоляции и повторного использования, поскольку он по ошибке имеет несколько обязанностей.
Проксирование атрибутов и перехват рендеринга в случае плохого фрагментированияHOC
Технологии могут заставить компоненты нести только одну ответственность.
Спасибо за вашу готовность потратить свое драгоценное время на чтение этой статьи. Если эта статья дала вам небольшую помощь или вдохновение, пожалуйста, не скупитесь на лайки и звезды. Вы, безусловно, самая большая движущая сила для меня, чтобы двигаться вперед . GitHub.com/Иветт Л.А. Ю/Б…