В этой статье поговорим о том, как написать в React многократно используемый код, используя три шаблона проектирования группы Gang-of-Four.
![](https://habrastorage.org/getpro/habr/upload_files/39d/3d9/2a6/39d3d92a607b84187e031e3406cdf437.jpeg)
Шаблоны проектирования позволяют создать повторно используемые и надежные решения распространенных проблем при разработке программного обеспечения. Использование шаблона проектирования может значительно сэкономить время при разработке и помочь быстрее внедрить новый элемент в производство.
Чтобы повысить производительность процесса, разработчикам важно понимать принципы работы шаблонов проектирования и особенности их использования.
В этой статье мы обсудим шаблоны проектирования группы Gang-of-Four и способы их реализации с помощью библиотеки React.
Gang-of-Four
Как уже упоминалось, шаблоны проектирования помогают найти решение типичных проблем при разработке программного обеспечения, независимое от языка, многократно используемое и масштабируемое. Название Gang of Four – «Банда четырех» – относится к четырем авторам, написавшим книгу «Design Patterns: Elements of Reusable Object-Oriented Software», в которой они предложили 23 шаблона проектирования, основанные на принципах объектно-ориентированного программирования.
Четыре автора разделили шаблоны проектирования на три группы.
1. Порождающие шаблоны позволяют создать экземпляр какого-либо класса (создать компонент), скрывая при этом логику его создания.
2. Структурные шаблоны помогают эффективно определять отношения между классами (компонентами).
3. Поведенческие шаблоны обеспечивают взаимодействие между компонентами.
Все эти шаблоны не зависят от языка. Разработчики могут адаптировать их к различным библиотекам и фреймворкам, таким как React например.
Использование шаблонов в React
Итак, у нас есть общее представление о шаблонах проектирования и о том, почему разработчикам стоит их использовать. Поэтому давайте посмотрим, как мы можем использовать эти шаблоны проектирования в React.
Для наглядности я рекомендую использовать React с TypeScript, так как это статически типизированный язык с интерфейсами, наследованием и поддержкой типов: все это помогает быстро приступить к использованию шаблонов.
Требования
Убедитесь, что у вас установлен Node.js. Вы можете набрать node -v, чтобы подтвердить установку. Если вы успешно установили Node.js, вы увидите такие строки.
![Подтверждение установки Node.js Подтверждение установки Node.js](https://habrastorage.org/getpro/habr/upload_files/7d2/d03/2ec/7d2d032ec5af3a79376b41bf1706fb6a.png)
После этого наберите команду ниже, чтобы создать проект React на TypeScript под названием “react-design-patterns.”
npx create-react-app react-design-patterns -template typescript
Singleton
Singleton — это порождающий шаблон, который гарантирует, что у класса будет только один экземпляр с одной точкой доступа.
Когда его стоит применять?
Например, предположим, что приложение React использует глобальный объект конфигурации, содержащий информацию о вошедшем в систему пользователе. Приложение должно повторно использовать эту информацию для получения информации о текущем пользователе в приложении.
Это идеальный сценарий, в котором разработчики могут использовать шаблон singleton.
Singleton в React
При реализации этого шаблона мы должны учитывать два условия.
1. Класс должен иметь только один элемент;
2. Он должен быть доступен только с одной точки.
В React разработчики могут использовать настраиваемый модуль управления для выполнения этих двух условий. Например, код ниже.
// user type
interface SingletonConfigValues {
name?: string;
id?: string;
email?: string;
token?: string;
}
// private variable accessible only within current module
let loggedInUserStore: SingletonConfigValues | undefined = undefined;
const userActions = {
// configure the single instance logged in user
initializeUser: (user: SingletonConfigValues) => {
loggedInUserStore = user;
},
// retrieve the single instance of the logged in user
getUserInformation: () => {
return loggedInUserStore;
}
};
// export the methods so that components can use the single instance
export default userActions;
Фрагмент выше показывает модуль TypeScript, который управляет состоянием единого экземпляра частной переменной – loggedInUserStore, используя два метода: getUserInformation – для получения единственного елемента и initializeUser — для его настройки.
Этот модуль обеспечивает одиночный подход к управлению глобальной пользовательской информацией. Чтобы использовать этот модуль в компоненте, импортируйте действия и вызовите методы, как показано ниже.
import { useEffect, useState } from "react";
// obtain the single instance - directly user var is not accessible (private)
import userRetriever from '../../store/custom-singleton';
export const ComponentA = () => {
const [userInformation, setUserInformation] = useState<any>(undefined);
useEffect(() => {
setUserInformation(userRetriever.getUserInformation());
}, [userInformation]);
return (
<div>
<h1>Component A</h1><p>
{userInformation && (
<>
<span>Name: {userInformation.name}</span><br />
<span>Id: {userInformation.id}</span><br />
<span>Email: {userInformation.email}</span>
<br />
<span>Token: {userInformation.token}</span>
</>
}
</p>
/div>
;
};
Фрагмент выше показывает компонент, который возвращает единственный элемент.
import { useEffect } from "react";
import { ComponentA } from "./component-a";
// retrieve the single instance - directly user var is not acessible (private)
import userInformation from '../../store/custom-singleton';
export const Singleton = () => {
useEffect(() => {
// initialize the single instance variable
userInformation.initializeUser({
name: 'John Doe',
id: '123',
email: 'johndoe@gmail.com',
token: '123456789'
})
}, []);
return (
<div className="App"><div><ComponentA /></div></div>
);
};
Фрагмент выше показывает единственный элемент пользователя, создаваемый в useEffect.
![](https://habrastorage.org/getpro/habr/upload_files/e1d/248/6df/e1d2486dfe79ec43b8ebf9ebfc2125db.png)
О преимуществах и недостатках
Использование этого шаблона позволяет эффективно масштабировать и поддерживать код, избежать его дублирования в нескольких компонентах.
Основным недостатком шаблона является то, что он делает код менее пригодным для тестирования, поскольку между модульными тестами глобальное состояние остается неизменным.
Observer или наблюдатель
Это поведенческий шаблон, который помогает определить механизм подписки для уведомления наблюдателей (компоненты) о любых изменениях объекта наблюдения.
Наиболее подходящий вариант использования в приложении React — когда приложение зависит от событий браузера. Например, в приложении React есть компонент, которому необходимо переключать свою функциональность в зависимости от доступности Интернета. Компонент может подписаться на события браузера, получать уведомления об изменениях и обновлять свое состояние в соответствии с ними.
Observer в React
При использовании шаблона:
1. Компонент должен сначала подписаться на событие.
2. Затем, когда компонент размонтируется, от события нужно отписаться.
Рассмотрим код ниже.
import { useEffect, useState } from "react";
export const InternetAvailabilityObserver = () => {
const [isOnline, setOnline] = useState<any>(navigator.onLine);
useEffect(() => {
// subscribe to two events -> online and offline
// when online -> set online to true
// when offline -> set online to false
window.addEventListener("online", () => setOnline(true));
window.addEventListener("offline", () => setOnline(false));
return () => {
// when component gets unmounted, remove the event listeners to prevent memory leaks
window.removeEventListener("online", () => setOnline(true));
window.removeEventListener("offline", () => setOnline(false));
};
}, []);
return (
<><h1>Internet Availability Observer</h1><p>
{isOnline ? (
<><span>
You are <b>online</b></span></>
) : (
<><span>
You are <b>offline</b></span></>
)}
</p>
</>
);
};
На фрагменте выше показано использование шаблона в React. Он создает две подписки с двумя субъектами (онлайн- и офлайн- события). И когда субъекты уведомляют о каких-либо изменениях, для обновления состояния компонента выполняются функции обратного вызова.
![Observer implementation with React Observer implementation with React](https://habrastorage.org/getpro/habr/upload_files/893/7a5/a05/8937a5a05a2c9a98b71e0ef2db2613aa.gif)
О преимуществах и недостатках
В качестве преимущества можно отметить, что он разделяет код между субъектом и наблюдателем, сопровождая код. Так же несколько наблюдателей могут подписаться и отказаться от подписки на субъект в любой момент.
К недостаткам можно случай, если при размонтировании компонента не удалить события, на которые вы были подписаны, React может продолжать отслеживать изменения, что приведет к утечке памяти и потере производительности.
Facade
Это структурный шаблон, цель которого предоставить упрощенный способ взаимодействия с несколькими компонентами через единый API. Кроме того, он скрывает сложность внутренних операций, делая код более читабельным. Для реализации этого шаблона вам потребуются интерфейсы и классы. Но вы можете использовать основной принцип этого шаблона и применить его в React.
Как его использовать?
Вы можете использовать этот шаблон в приложениях React, когда структура приложения становится сложной. Хорошим примером использования шаблона в React является управление состоянием компонента.
Например, у вас может быть компонент, который управляет пользователями приложения (добавление, удаление, выборка). Как правило, это приводит к большому количеству переменных состояния и HTTP-запросов, которые загромождают код и ухудшают его читабельность. Рассмотрим фрагмент ниже.
export const NoFacade: FC = () => {
const [users, setUsers] = useState<any>([]);
const fetchUsers = useCallback(async () => {
const resp = await axios.get(`/api/users`);
setUsers(resp.data);
}, []);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const handleUserDelete = async (id: string) => {
await axios.delete(`/api/users/${id}`);
setUsers(users.filter((user: any) => user.id !== id));
};
const handleCreateUser = async (user: any) => {
if (!users.find((u: any) => u.id === user.id)) {
await axios.post(`/api/users`, user);
setUsers([...users, user]);
} else {
console.log("User already exists");
}
};
return (
<><UserTable users={users} onDelete={handleUserDelete} />
<UserCreateModal onCreate={handleCreateUser} /></>
);
};
На первый взгляд фрагмент сложно прочитать, так как он состоит из сложных HTTP-запросов и переменных состояния. Однако разработчики могут сгруппировать сложные HTTP-запросы и переменные состояния и оставить единый API для управления данными. В таких случаях можно использовать этот шаблон.
В React разработчики могут прийти к тому же результату, сгруппировав сложный код в кастомный хук и передав его во владение клиентов.
Facade в React
Улучшим фрагмент, приведенный выше, заменив сложный код на кастомный хук. Он будет содержать методы для создания, извлечения и удаления пользователей. Компоненты могут применять методы для использования функциональности, не просматривая сложный код, который выполняется последовательно.
Код кастомного элемента показан ниже.
import axios from "axios";
import { useState } from "react";
export const useFacadeUserAPI = () => {
const [users, setUsers] = useState<any>([]);
const [actionExecuting, setActionExecuting] = useState<boolean>(false);
// expose one method to get users
async function getUsers() {setActionExecuting(true);
try {
const resp = await axios.get("/api/users");
setUsers(resp.data);
} catch (err) {
console.log(err);
} finally {
setActionExecuting(false);
}
}
// expose method to create user
async function createUser(user: any) {setActionExecuting(true);
try {
await axios.post("/api/users", user);
setUsers([...users, user]);
} catch (err) {
console.log(err);
} finally {
setActionExecuting(false);
}
}
// expose method to delete a user
async function deleteUser(id: string) {setActionExecuting(true);
try {
await axios.delete(`/api/users/${id}`);
setUsers(users.filter((user: any) => user.id !== id));
} catch (err) {
console.log(err);
} finally {
setActionExecuting(false);
}
}
// return the methods that encapsulate the complex code// resulting
in a cleaner client code
return {// the users
users,
// boolean to indicate if action occurs
actionExecuting,
// method to mutate the users via HTTP requests
getUsers,
createUser,
deleteUser,
};
};
На фрагменте выше показан кастомный хук, который отвечает за управление пользователями. Он возвращает набор методов, которые компоненты могут использовать для взаимодействия с API.
Обновленный код компонента показан ниже.
export const Facade: FC = () => {
const userFacade = useFacadeUserAPI();
const { createUser, deleteUser, getUsers, users } = userFacade;
const fetchUsers = useCallback(async () => {
// replace with facade API method to simplify code
await getUsers();
}, [getUsers]);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const handleUserDelete = async (id: string) => {
// replace with Facade method to hide complex code required in deleting
await deleteUser(id);
};
const handleCreateUser = async (user: any) => {
// replace with a facade method to hide complex code required in creating
await createUser(user);
};
return (
<><UserTable users={users} onDelete={handleUserDelete} />
<UserCreateModal onCreate={handleCreateUser} /></>
);
};
На фрагменте выше показан обновленный код для компонента. Кастомный хук становится менее запутанным и более понятным, а также полностью скрывает всю сложную бизнес-логику компонента.
О преимуществах и недостатках
В плюс то, что он позволяет повторно использовать код, что помогает избежать его дублирования, а также разделяет бизнес-логику компонента, позволяя разработчикам писать модульные тесты для функций.
При этом, трудно перепроектировать существующий код и заключить его в кастомный хук.
Но при правильной реализации шаблон создает переиспользуемый и масштабируемый код.
Заключение
В этой статье мы рассмотрели шаблоны проектирования и то, как разработчики могут использовать их в библиотеке React для совершенствования фронтенд-разработки. Шаблоны проектирования, реализованные в этой статье, доступны в репозитории.