Composition Pattern

Composition (Композиция) — архитектурный принцип построения объектов путём комбинирования небольших независимых функций/объектов вместо наследования («composition over inheritance»).

Зачем нужно

Наследование создаёт жёсткую иерархию: чтобы добавить поведение листовому классу, нужно менять базовый. Композиция гибче: объект собирается из независимых «способностей» (mixins, traits, behaviours). Это соответствует принципу «prefer composition over inheritance» из GoF. В JavaScript функциональная композиция особенно естественна благодаря функциям первого класса.

Где используется

  • React Hooks: useFetch, useLocalStorage — поведение вместо миксинов класса
  • Mixins для объектов: добавление serializable, loggable, observable поведения
  • Функциональная композиция: compose(f, g, h)(x) = f(g(h(x)))
  • Middleware-цепочки: Express, Koa, Redux middleware
  • Entity Component System (ECS) в играх: сущность = набор компонентов

Основной контент

Объектная композиция через mixins

// Независимые «способности» как объекты-поведения
const Serializable = {
  serialize {
    return JSON.stringify(this);
  },
  deserialize(data) {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), JSON.parse(data));
  }
};

const Loggable = {
  log(message) {
    console.log(`[${this.constructor.name}] ${new Date.toISOString()}: ${message}`);
  }
};

const Validatable = {
  validate {
    return Object.entries(this.rules || {}).every(([field, rule]) => rule(this[field]));
  }
};

// Компоновка объекта из поведений
function createUser(data) {
  return Object.assign(
    Object.create(null),
    Serializable,
    Loggable,
    Validatable,
    {
      ...data,
      rules: {
        name: (v) => v && v.length >= 2,
        email: (v) => /^[\w.]+@\w+\.\w+$/.test(v)
      }
    }
  );
}

const user = createUser({ name: 'Иван', email: 'ivan@example.com' });
user.log('Создан'); // [Object] 2026-04-10T...: Создан
console.log(user.validate); // true
console.log(user.serialize); // JSON-строка

Функциональная композиция

// compose: применяет функции справа налево
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);

// pipe: применяет функции слева направо (удобнее для чтения)
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

// Трансформация данных через цепочку чистых функций
const trim = (s) => s.trim();
const toLowerCase = (s) => s.toLowerCase();
const removeSpaces = (s) => s.replace(/\s+/g, '-');
const truncate = (max) => (s) => s.slice(0, max);

const toSlug = pipe(trim, toLowerCase, removeSpaces, truncate(50));

console.log(toSlug('  Hello World Привет  ')); // 'hello-world-привет'

// Composable middleware
const withLogging = (fn) => async (...args) => {
  console.log('Вызов с', args);
  const result = await fn(...args);
  console.log('Результат:', result);
  return result;
};

const withRetry = (fn, retries = 3) => async (...args) => {
  for (let i = 0; i < retries; i++) {
    try { return await fn(...args); }
    catch (e) { if (i === retries - 1) throw e; }
  }
};

const fetchUser = async (id) => { /* ... */ };
const robustFetchUser = withRetry(withLogging(fetchUser), 3);

Composition в React (Custom Hooks)

// Вместо HOC и миксинов классов — composable hooks
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    fetch(url).then(r => r.json()).then(d => { setData(d); setLoading(false); });
  }, [url]);
  return { data, loading };
}

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    try { return JSON.parse(localStorage.getItem(key)) ?? initial; }
    catch { return initial; }
  });
  const set = (v) => { setValue(v); localStorage.setItem(key, JSON.stringify(v)); };
  return [value, set];
}

// Компонент использует оба поведения независимо
function UserProfile({ userId }) {
  const { data: user, loading } = useFetch(`/api/users/${userId}`);
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  // ...
}

Частые ошибки

  • Глубокий Object.assign без клонирования: ссылочные типы в миксинах разделяются между всеми экземплярами — всегда проверяйте вложенные объекты.
  • Ромбовидная проблема в миксинах: если два миксина определяют один метод, последний перезаписывает первый без предупреждения.
  • Чрезмерная композиция: pipe(a, b, c, d, e, f, g) из 10 функций так же нечитаема, как глубокая иерархия классов. Группируйте в осмысленные функции.

Связанные темы

Ресурсы