Синглтон

В 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;

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

Заключение

Я попытался добавить простые и понятные реальные варианты использования шаблонов проектирования во фронтенде. Я надеюсь, что это было полезно.

Если у вас есть какие-либо предложения, не стесняйтесь добавлять комментарии.