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 функций так же нечитаема, как глубокая иерархия классов. Группируйте в осмысленные функции.