Синглтон
В JavaScript обычных классов не существует, это синтаксический сахар, в основном, когда вы создаете обычный объект в JS, он уже является синглтоном, потому что, если вы собираетесь создать точно такой же объект, он не будет равен (просто потому, что вы сравниваете ссылки с кучей).
Вот пример, где я использую синглтон (es5):
function ToastMessage() { if (ToastMessage.instance) { return ToastMessage.instance; } this.basePhrase = 'Message Toast: '; ToastMessage.instance = this; } ToastMessage.prototype.toastMessage = function(message, type) { const { background, color } = this.getColorToast(type); return console.log( `%c${this.basePhrase + message}`, `background: ${background}; color: ${color}` ); }; ToastMessage.prototype.getColorToast = function(type) { switch (type) { case 'error': return { background: 'black', color: 'red', }; case 'warning': return { background: 'transparent', color: 'orange', }; default: return { background: 'black', color: 'green', }; } }; const toastMessageErrorInstance = new ToastMessage(); const toastMessageAcceptedInstance = new ToastMessage(); console.log('is it equal?', toastMessageErrorInstance === toastMessageAcceptedInstance); toastMessageErrorInstance.toastMessage('Sending message failed', 'error'); toastMessageAcceptedInstance.toastMessage('Message send', 'accepted');
Для плагинов Chrome, когда мне нужно уведомить пользователя о некоторых изменениях в разных частях проекта, я могу использовать один экземпляр с одинаковым поведением для сообщений.
Тот же пример с использованием классов
class ToastMessage { static #instance = null; #basePhrase; constructor() { if (ToastMessage.instance) { return ToastMessage.instance; } this.basePhrase = 'Message Toast:'; ToastMessage.instance = this; } static getInstance() { return ToastMessage.instance || new ToastMessage(); } toastMessage(message, type) { const { background, color } = this.#getColorToast(type); return console.log( `%c${this.basePhrase + message}`, `background: ${background}; color: ${color}` ); } #getColorToast(type) { switch (type) { case 'error': return { background: 'black', color: 'red', }; case 'warning': return { background: 'black', color: 'orange', }; default: return { background: 'black', color: 'green', }; } } } const alertFalse = new ToastMessage(); const alertTrue = new ToastMessage(); console.log('is it equal?', alertFalse === alertTrue); alertFalse.toastMessage('Sending message failed', 'error'); alertTrue.toastMessage('Message send', 'accepted');
В React вы можете использовать Singleton для создания контекста:
// Create a Singleton store const StoreContext = createContext(); const StoreProvider = ({ children }) => { const [count, setCount] = useState(0); return ( <StoreContext.Provider value={{ count, setCount }}> {children} </StoreContext.Provider> ); }; // Custom hook to access the Singleton store const useStore = () => { const context = useContext(StoreContext); if (!context) { throw new Error('useStore must be used within a StoreProvider.'); } return context; }; const ChildComponent = () => { const { count, setCount } = useStore(); ... });
Фабрика
Основное назначение шаблона Factory для создания объектов с похожими повторяющимися операциями.
Ярким примером шаблона Factory в JS является глобальный конструктор Object. Он ведет себя как ткань, потому что создает различные типы объектов на основе входных данных.
const obj = new Object(); const number = Object(1); const string = Object('string'); const boolean = Object(true); console.log(obj.constructor === Object); console.log(number.constructor === Number); const numberConstr = Number(1); const stringConstr = String('string'); const booleanConstr = Boolean(true); console.log(string.constructor === stringConstr.constructor); console.log(number.constructor === numberConstr.constructor);
Объявите переменную Object(1), идентичную Number(1), чтобы под капотом конструктор Object вел себя как ткань для разных типов данных.
Пример реакции для фабричного шаблона:
import React from 'react'; // Chart Factory function createChart(chartType, data) { switch (chartType) { case 'bar': return new BarChart(data); case 'line': return new LineChart(data); case 'pie': return new PieChart(data); default: return null; } } // Chart Classes (Using a common 'draw' method) function BarChart(data) { this.data = data; this.draw = function () { console.log('Drawing Bar Chart with data:', this.data); // Logic to draw a Bar Chart with 'data' }; } function LineChart(data) { this.data = data; this.draw = function () { console.log('Drawing Line Chart with data:', this.data); // Logic to draw a Line Chart with 'data' }; } function PieChart(data) { this.data = data; this.draw = function () { console.log('Drawing Pie Chart with data:', this.data); // Logic to draw a Pie Chart with 'data' }; } // Chart Component in React function Chart({ chartType, chartData }) { const chart = createChart(chartType, chartData); React.useEffect(() => { if (chart) { chart.draw(); } }, [chart]); return </>; } // Usage const chartData = [10, 20, 15, 30]; const userChartType = 'bar'; function App() { return ( <div> <h1>Chart Library</h1> <Chart chartType={userChartType} chartData={chartData} /> </div> ); } export default App;
Здесь я создаю базовую фабрику диаграмм для использования различных типов диаграмм с одной и той же абстракцией (метод рисования).
Декоратор
Когда мы используем паттерн Decorator, мы можем динамически добавлять дополнительные функции к существующим объектам.
Простой пример декораторов в JS — функция memoize:
function memoize(fn) { const cache = new Map(); return function (...args) { const key = args.join('-'); if (cache.has(key)) { console.log('Cached'); return cache.get(key); } else { const result = fn(...args); cache.set(key, result); return result; } }; } function getFibonacci(n) { if (n <= 1) return n; return getFibonacci(n - 1) + getFibonacci(n - 2); } const cachedCall = memoize(getFibonacci); console.log(cachedCall(14));
Мы добавляем поведение к существующей функции getFibonacci для кэширования результата для тех же входных данных.
Пример реакции:
import React, { useEffect, useState } from 'react'; // Higher-order component (HOC) - Decorator function loadDataWithSpinner(Wrapper, fetchDataFn) { return function WithLoadingSpinner(props) { const [isLoading, setIsLoading] = useState(true); const [data, setData] = useState(null); useEffect(() => { fetchDataFn() .then(response => { setData(response); }) .catch(error => { console.error('Error:', error); }) .finally(() => { setIsLoading(false); }); }, []); if (isLoading) { return <div>Loading...</div>; } return <Wrapper data={data} {...props} />; }; } // Component that will be wrapped by the HOC function DataComp({ data }) { return <div>{data && data.map(item => <div key={item.id}>{item.name}</div>)}</div>; } // Simulated API fetch function const fetchDataFromAPI = () => new Promise(resolve => setTimeout( () => resolve([ { id: 1, name: 'Item 1' }, ]), 500 ) ); // Usage with decorator const DataWithSpinner = loadDataWithSpinner(DataComp, fetchDataFromAPI); function App() { return ( <div> <DataWithSpinner /> </div> ); } export default App;
В React HOC компонент более высокого порядка является базовой реализацией шаблона Decorator. Вот пример, когда в HOC мы добавляем загрузку для каждого дочернего элемента при загрузке данных.
Стратегия
Этот паттерн позволяет выбирать тот или иной алгоритм во время выполнения.
Вы можете работать с одним и тем же интерфейсом и выбирать алгоритм, наиболее подходящий для решения конкретной задачи.
Использование паттерна Стратегия для обработки платежей:
// Payment strategies const paymentStrategies = { paypal: { processPayment: amount => { // PayPal payment processing logic console.log(`Processing PayPal payment for $${amount}`); }, }, stripe: { processPayment: amount => { // Stripe payment processing logic console.log(`Processing Stripe payment for $${amount}`); }, }, square: { processPayment: amount => { // Square payment processing logic console.log(`Processing Square payment for $${amount}`); }, }, }; function processPayment(paymentGateway, amount) { const paymentStrategy = paymentStrategies[paymentGateway]; if (paymentStrategy) { paymentStrategy.processPayment(amount); } else { throw new Error('Invalid payment gateway selected.'); } } // Usage: processPayment('paypal', 100); processPayment('stripe', 50);
Для разных способов оплаты авторизация с разным API, поэтому с помощью паттерна Стратегия мы можем динамически переключаться между ними в зависимости от выбора пользователя.
Используя шаблон стратегии, мы можем проверить поля формы в React:
import React, { useState } from 'react'; // Validation strategies for each form field const validationStrategies = { username: { validate: value => /^[a-zA-Z0-9_]{3,20}$/.test(value), errorMessage: 'Username must be 3 to 20 characters long and can only contain letters, numbers, and underscores.', }, password: { validate: value => value.length >= 6, errorMessage: 'Password must be at least 6 characters long.', }, }; const FormField = ({ label, value, onChange, onBlur, errorMessage }) => { return ( <div> <label>{label}</label> <input type="text" value={value} onChange={onChange} onBlur={onBlur} /> {errorMessage && <span style={{ color: 'red' }}>{errorMessage}</span>} </div> ); }; const LoginForm = () => { const [username, setUsername] = useState(''); const [usernameError, setUsernameError] = useState(''); const [password, setPassword] = useState(''); const [passwordError, setPasswordError] = useState(''); const validateField = (fieldName, value) => { const strategy = validationStrategies[fieldName]; if (strategy && strategy.validate(value)) { return ''; } return strategy?.errorMessage || ''; }; const handleUsernameChange = e => { const value = e.target.value; setUsername(value); const errorMessage = validateField('username', value); setUsernameError(errorMessage); }; const handlePasswordChange = e => { const value = e.target.value; setPassword(value); const errorMessage = validateField('password', value); setPasswordError(errorMessage); }; const handleFormSubmit = e => { e.preventDefault(); const usernameError = validateField('username', username); const passwordError = validateField('password', password); setUsernameError(usernameError); setPasswordError(passwordError); if (!usernameError && !passwordError) { // Submit the form console.log('Form submitted successfully!'); } }; return ( <form onSubmit={handleFormSubmit}> <FormField label="Username" value={username} onChange={handleUsernameChange} onBlur={() => setUsernameError(validateField('username', username))} errorMessage={usernameError} /> <FormField label="Password" value={password} onChange={handlePasswordChange} onBlur={() => setPasswordError(validateField('password', password))} errorMessage={passwordError} /> <button type="submit">Login</button> </form> ); }; export default LoginForm;
Шаблон стратегии можно использовать для определения различных стратегий проверки для разных полей формы. Каждая стратегия проверки инкапсулирует определенную логику проверки, и форма может динамически переключаться между этими стратегиями в зависимости от ввода пользователя или других условий.
Заключение
Я попытался добавить простые и понятные реальные варианты использования шаблонов проектирования во фронтенде. Я надеюсь, что это было полезно.
Если у вас есть какие-либо предложения, не стесняйтесь добавлять комментарии.