Наблюдатель

Pattern Observer широко используется в JS. document.addEventListener — это пример шаблона Observer в чистом JS, где вы подписываетесь на события и обрабатываете их с помощью прослушивателей событий:

// Observer 1
function handleClick1() {
  console.log('Button clicked (Observer 1)');
}

// Observer 2
function handleClick2() {
  console.log('Button clicked (Observer 2)');
}

// Subscribe observers to the click event
document.addEventListener('click', handleClick1);
document.addEventListener('click', handleClick2);

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

Другой пример шаблона Observer — MutationObserver. Это позволяет вам наблюдать и реагировать на изменения в DOM (объектной модели документа), подписываясь на определенные мутации DOM.

const targetNode = document.getElementById('target');

// Observer: Handle DOM mutations
const observer = new MutationObserver((mutationsList, observer) => {
  for (const mutation of mutationsList) {
    if (mutation.type === 'childList') {
      console.log('DOM Mutation occurred - childList');
    } else if (mutation.type === 'attributes') {
      console.log('DOM Mutation occurred - attributes');
    }
   }
 });

// Options for the observer (specify what to observe)
const config = { childList: true, attributes: true };

// Start observing the target node
observer.observe(targetNode, config);

// Trigger some DOM mutations for demonstration purposes
targetNode.appendChild(document.createElement('span'));
targetNode.setAttribute('data-custom-attr', 'custom-value');

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

Мы можем считать, что WebSocket также является примером шаблона Observer.

На стороне сервера:

//Server

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

const subscribers = new Set();

wss.on('connection', (ws) => {
  // Add new client to the subscribers set
  subscribers.add(ws);

  // Observer: Handle incoming messages from clients
  ws.on('message', (data) => {
    console.log(`Received message from client: ${data}`);
  });

  // Notify observer about an event (e.g., every 5 seconds)
  setInterval(() => {
    broadcast('Server sending data to all clients');
  }, 5000);

  // Remove the client from subscribers when disconnected
  ws.on('close', () => {
    subscribers.delete(ws);
  });
});

function broadcast(message) {
  // Send message to all connected clients (subscribers)
  for (const client of subscribers) {
    client.send(message);
  }
}

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

Сторона клиента:

const ws = new WebSocket('ws://localhost:8080');

// Observer: Handle incoming messages from the server
ws.onmessage = (event) => {
 console.log(`Received from server: ${event.data}`);
};

В React типичным случаем использования шаблона Observer является управление состоянием. Популярные библиотеки управления состоянием, такие как Redux и MobX, используют шаблон Observer для управления состоянием приложения. Компоненты подписываются на хранилище состояний, и когда состояние обновляется, все подписанные компоненты уведомляются и перерисовываются с последним состоянием.

// Redux-like store
const createStore = (reducer) => {
  let state;
  let subscribers = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    subscribers.forEach((subscriber) => subscriber());
  };

  const subscribe = (subscriber) => {
    subscribers.push(subscriber);
    return () => {
      subscribers = subscribers.filter((sub) => sub !== subscriber);
    };
  };

  dispatch({}); // Initialize state

  return { getState, dispatch, subscribe };
};

// Reducer
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'RESET':
      return 0;
    default:
      return state;
  }
};

// Create a store using the reducer
const store = createStore(counterReducer);

// React Counter Component subscribing to the store
function Counter() {
  const [count, setCount] = useState(store.getState());

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setCount(store.getState());
    });
    return () => {
      unsubscribe();
    };
  }, []);

  const handleIncrement = () => {
    store.dispatch({ type: 'INCREMENT' });
  };

  const handleReset = () => {
    store.dispatch({ type: 'RESET' });
  };

  return (
    <div>
      <h2>Counter</h2>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

Система обработки событий React также использует шаблон Observer. Компоненты регистрируют прослушиватели событий для определенных событий так же, как и в чистом JS, и когда эти события происходят, подписанные компоненты получают обновления и реагируют соответствующим образом.

Прокси

В чистом JS есть объект Proxy, который позволяет вам создать объект, который можно использовать вместо исходного объекта.

function expensiveFunction(n) {
  console.log(`Computing for ${n}...`);
  // Simulating some expensive computation
  return n * 2;
}

function createMemoizationProxy(targetFunction) {
  const cache = new Map();

  return new Proxy(targetFunction, {
    apply(target, thisArg, args) {
      const key = args.toString();
      if (cache.has(key)) {
        console.log(`Returning cached result for ${args}`);
        return cache.get(key);
      }

      const result = target.apply(thisArg, args);
      cache.set(key, result);
      return result;
    },
  });
}

const memoizedExpensiveFunction = createMemoizationProxy(expensiveFunction);

console.log(memoizedExpensiveFunction(5));
console.log(memoizedExpensiveFunction(10));
console.log(memoizedExpensiveFunction(5));

Мы можем использовать шаблон Proxy для создания прокси-сервера мемоизации, который автоматически кэширует результаты функции и возвращает кэшированный результат, если те же аргументы предоставляются снова.

В React хорошим примером паттерна Proxy может быть Debounce:

import React, { useState, useEffect } from 'react';

// Simulating backend request for data
function fetchData(selectedCheckboxes) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const data = selectedCheckboxes.map((checkbox) => ({
        name: `Data for checkbox ${checkbox}`,
      }));
      resolve(data);
    }, 500);
  });
}

function Checkbox({ label, checked, onChange }) {
  return (
    <div>
      <input type="checkbox" checked={checked} onChange={onChange} />
      <label>{label}</label>
    </div>
  );
}

function App() {
  const [selectedCheckboxes, setSelectedCheckboxes] = useState([]);
  const [data, setData] = useState([]);

  // Debounce function to optimize backend calls
  const debounce = (func, delay) => {
    let timeoutId;
    return (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func(...args), delay);
    };
  };

  // Fetch data when selectedCheckboxes change
  useEffect(() => {
    const fetchDataDebounced = debounce(() => {
      const promises = selectedCheckboxes.map((checkboxId) => fetchData(checkboxId));

      Promise.all(promises).then((responseData) => {
        setData(responseData);
      });
    }, 100); // Debounce the request for 100ms
  }, [selectedCheckboxes]);

  const handleCheckboxChange = (event, checkbox) => {
    const isChecked = event.target.checked;
    if (isChecked) {
      setSelectedCheckboxes([...selectedCheckboxes, checkbox]);
    } else {
      setSelectedCheckboxes(selectedCheckboxes.filter((cb) => cb !== checkbox));
    }
  };

  return (
    <div>
      <Checkbox
        label="Checkbox 1"
        checked={selectedCheckboxes.includes(1)}
        onChange={(e) => handleCheckboxChange(e, 1)}
      />
      <Checkbox
        label="Checkbox 2"
        checked={selectedCheckboxes.includes(2)}
        onChange={(e) => handleCheckboxChange(e, 2)}
      />
      <Checkbox
        label="Checkbox 3"
        checked={selectedCheckboxes.includes(3)}
        onChange={(e) => handleCheckboxChange(e, 3)}
      />
      <Checkbox
        label="Select All"
        checked={selectedCheckboxes.length === 3}
        onChange={(e) =>
          e.target.checked
            ? setSelectedCheckboxes([1, 2, 3])
            : setSelectedCheckboxes([])
        }
      />
      <div>
        {data.map((item, index) => (
          <p key={index}>{item.name}</p>
        ))}
      </div>
    </div>
  );
}

export default App;

Например, у нас есть три флажка, которые отправляют запросы данных при нажатии, и вы хотите оптимизировать внутренние вызовы с помощью прокси-запроса, который ожидает 100 мс, чтобы объединить несколько флажков в один запрос, вы можете использовать хуки React useState и useEffect для управления состояние флажка и шаблон устранения отказов для прокси-запроса.

Фасад

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

// Complex API
class API {
  static fetchData(endpoint) {
    console.log(`Fetching data from ${endpoint}`);
    return fetch(endpoint).then((response) => response.json());
  }
}

// Facade for the API
class APIFacade {
  static getUserData() {
    return API.fetchData('/api/users');
  }

  static getProductData() {
    return API.fetchData('/api/products');
  }
}

// Usage
APIFacade.getUserData().then((userData) => console.log(userData));
APIFacade.getProductData().then((productData) => console.log(productData));

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

В React шаблон Facade часто используется для предоставления простого интерфейса сложной подсистеме компонентов. Этот шаблон способствует простоте и уменьшает зависимости между компонентами.

import React, { useState } from 'react';

// Subcomponents that handle specific aspects of the form
import NameInput from './NameInput';
import EmailInput from './EmailInput';
import AgeInput from './AgeInput';
import SubmitButton from './SubmitButton';

// Facade Component
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');

  const handleSubmit = () => {
    console.log('Submitted Form Data:', { name, email, age });
  };

  return (
    <div>
      <h2>Form</h2>
      <NameInput value={name} onChange={setName} />
      <EmailInput value={email} onChange={setEmail} />
      <AgeInput value={age} onChange={setAge} />
      <SubmitButton onClick={handleSubmit} />
    </div>
  );
}

export default ComplexForm;

В этом примере компонент Form действует как фасад и инкапсулирует сложную логику формы и несколько полей ввода (NameInput, EmailInput, AgeInput) и SubmitButton. Другие компоненты приложения могут использовать компонент Form, не беспокоясь о внутренних деталях работы формы или об обработке данных.

Заключение

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

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