Скрытые сокровища объектной композиции
Примечание. Это часть серии «Composing Software» по изучению функционального программирования и техник композиции в JavaScript ES6 + с нуля. Будьте на связи. Впереди много интресного!
“Предпочитайте композицию объектов классовому наследованию” ~ Банда четырех, “Design Patterns”.
Одной из наиболее распространенных ошибок в разработке программного обеспечения является тенденция чрезмерного использования наследования классов. Наследование класса - это механизм повторного использования кода, где экземпляры формируются в is-a соотношении с базовыми классами. Если у вас возникает соблазн описывать свою область, используя is-a отношения (например, утка is-a птица), у вас будут неприятности, потому что наследование классов является самой узкой формой связи, доступной в объектно-ориентированной разработке, что в итоге приводит к общим проблемам, например к таким:
- Проблема слабого базового класса
- Проблема гориллы и банана
- Проблема неизбежного дублирования
Наследование классов позволяет переиспользовать код путем абстракции общего интерфейса в базовый класс. Дочерние классы могут наследовать базовый, добавлять и переопределять его интерфейс. Есть две важные части абстракции:
- Обобщение - процесс выделения только общих свойств и поведения, которые подходят для общего использования
- Специализация - процесс имплементации функционала, необходимого для частных случаев
Существует множество способов реализовать обобщение и специализацию в коде. Есть несколько хороших альтернатив классовому наследованию, например - простые функции, функции высшего порядка, объектная композиция.
К сожалению, объектная композиция очень недопонята, т.к. многие пытаются мыслить с точки зрения композиции объекта. Пришло время изучить тему чуть глубже.
Что такое объектная композиция?
«В информатике - составной или смешанный тип данных - это любой тип данных, который может быть собран в программе с использованием примитивных типов данных языка программирования и других составных типов. ... Акт построения композитного типа известен как композиция. "~ Википедия
Одной из причин путаницы, связанной с объектной композицией, является то, что любая сборка примитивных типов для формирования составного объекта является формой объектной композиции, но методы наследования часто обсуждаются отдельно от композиции, как если бы они были разными. Причиной двойственного смысла является то, что существует различие между грамматикой и семантикой термина.
При обсуждении объектной композиции и классового наследования мы говорим не о конкретных методах: мы говорим о семантических связях и степени сцепления между компонентами объекта. Мы говорим о значении, а не о грамматике. Люди часто не могут понять это и грязнут в грамматических деталях - они упускают главное из-за мелочей.
Существует много разных способов объектной композиции. Различные формы композиции будут создавать различные составные структуры и различные отношения между объектами. Когда объекты зависят от других, с которыми они находятся в каком-то отношении - эти объекты связаны, что означает, изменение одного объекта может сломать другой.
Совет «Банды четырех», который предлагает «предпочесть композицию объекта классовому наследованию», предлагает нам думать о наших объектах как о композиции меньших, слабо связанных объектов, а не о общем наследовании от монолитного базового класса. Банда Четырех описывает тесно связанные объекты как «монолитные системы, где вы не можете изменить или удалить класс без понимания и изменения многих других классов. Система становится плотной массой, которую трудно понимать, портировать и поддерживать».
Три формы объектной композиции
В «Design Patterns», «Банда четырех» утверждает, «вы увидите, что объектная композиция применяется снова и снова в шаблонах проектирования» и далее описывает различные типы композиционных отношений, включая агрегацию и делегирование.
Авторы «Design Patterns» в основном работали с C++ и Smalltalk (позже Java). Построение и изменение отношений объектов во время выполнения на этих языках намного сложнее, чем в JavaScript, поэтому они, по понятным причинам, не содержат много деталей по этому вопросу. Однако, обсуждение объектной композиции в JavaScript не было бы полным без обсуждения динамического расширения объекта, а также конкатенации.
По причинам интерпретации к JavaScript и к созданию более простых обобщений мы немного расходимся с определениями, используемыми в «Шаблонах проектирования». Например, мы не будем требовать, чтобы аггрегирование подразумевало контроль над жизненными циклами подобъектов. Это просто неверно для языка с динамическим расширением объектов.
Выбор неправильных аксиом может излишне ограничивать полезное обобщение и заставлять нас по-другому называть особые случаи той же общей идеи. Разработчики программного обеспечения не любят повторяться, когда это не нужно.
- Агрегация - когда объект формируется из перечислимого набора подобъектов. Другими словами, объект, который содержит другие объекты. Каждый подобъект сохраняет свой собственный ссылочный идентификатор, так что он может быть вычленен из агрегации без потери информации.
- Конкатенация - когда объект формируется путем добавления новых свойств к существующему объекту. Свойства могут быть объединены по одному или скопированы из существующих объектов, например, плагины jQuery создаются путем объединения новых методов в прототип делегата jQuery - jQuery.fn.
- Делегирование - когда объект ссылается или делегирует другому объекту. Например, Альбом Эван Сазерленд (1962) включал экземпляры со ссылками на «мастера», которые были делегированы для целей осведомления. Photoshop включает в себя «умные объекты», которые служат локальными прокси-серверами, которые делегируют к внешнему ресурсу. Прототипы JavaScript также являются делегатами: экземпляры массива перенаправляют встроенные методы массива на Array.prototype, объекты - на Object.prototype и т. д.
Важно отметить, что эти разные формы композиции не являются взаимоисключающими. Можно реализовать делегирование с использованием агрегации, а наследование классов реализовано с использованием делегирования в JavaScript. Многие программные системы используют более одного типа композиции, например, плагины jQuery используют конкатенацию для расширения прототипа делегата jQuery - jQuery.fn. Когда клиентский код вызывает метод плагина, запрос делегируется методу, который был сконкатенирован (объединен) с прототипом делегата.
Примечание к примерам кода В примерах кода ниже будет приведен следующий начальный код:
const objs = [
{ a: 'a', b: 'ab' },
{ b: 'b' },
{ c: 'c', b: 'cb' }
];
Агрегация
Агрегация - это когда объект формируется из перечислимого набора подобъектов. Агрегат - это объект, который содержит другие объекты. Каждый подобъект в агрегации сохраняет свой собственный ссылочный идентификатор и может быть без потерь вычленен из совокупности. Агрегаты могут быть представлены в самых разных структурах.
Примеры:
- Массивы
- мапы
- множества
- графы
- деревья
- Узлы DOM (узел DOM может содержать дочерние узлы)
- Компоненты пользовательского интерфейса (компонент может содержать дочерние компоненты)
Когда использовать
Всякий раз, когда есть коллекции объектов, которые должны совместно использовать общие операции, такие как итерации, стеки, очереди, деревья, графы, конечные машины или составной шаблон (если вы хотите, чтобы один элемент имел тот же интерфейс, что и множество элементов).
Соображения
Агрегации отлично подходят для применения универсальных абстракций, таких как применение функции к каждому члену агрегата (например, array.map (fn)), преобразование векторов, как если бы они были одиночными значениями, и так далее. Однако, если есть потенциально сотни тысяч или миллионы подобъектов, потоковая обработка может быть более эффективной.
Примеры кода
Аггрегация массивов
const collection = (a, e) => a.concat([e]);
const a = objs.reduce(collection, []);
console.log(
'collection aggregation',
a,
a[1].b,
a[2].c,
`enumerable keys: ${ Object.keys(a) }`
);
В результате даст:
collection aggregation
[{"a":"a","b":"ab"},{"b":"b"},{"c":"c","b":"cb"}]
b c
enumerable keys: 0,1,2
Аггрегация связного списка используя пары:
const pair = (a, b) => [b, a];
const l = objs.reduceRight(pair, []);
console.log(
'linked list aggregation',
l,
`enumerable keys: ${ Object.keys(l) }`
);
/*
linked list aggregation
[
{"a":"a","b":"ab"}, [
{"b":"b"}, [
{"c":"c","b":"cb"},
[]
]
]
]
enumerable keys: 0,1
*/
Связанные списки составляют основу множества других структур данных и агрегаций, таких как массивы, строки и различные типы деревьев. Существует много других возможных агрегатов. Мы не будем подробно описывать их здесь.
Конкатенация
Конкатенация - это когда объект формируется путем добавления новых свойств к существующему объекту.
Примеры
- Плагины добавляются в jQuery.fn через конкатенацию
- Редукторы состояния (например, Redux)
- Функциональные миксины
Когда использовать
В любое время когда было бы полезно постепенно собирать структуры данных во время выполнения, например, объединять объекты JSON, собирать состояние приложения из нескольких источников, создавая обновления через неизменяемое состояние (путем слияния предыдущего состояния с новыми данными) и т. д.
Соображения
Будьте осторожны, мутируя существующие объекты. Совместное изменение состояние - это рецепт многих ошибок.
Можно имитировать иерархии классов и is-a cоотношения с конкатенацией. Проблемы будут те же. Подумайте о том, как создавать небольшие независимые объекты, а не наследовать параметры из «базового» экземпляра и применять дифференциальное наследование.
Остерегайтесь неявных межкомпонентных зависимостей.
Коллизии имен разрешаются порядком конкатенации: выигрывает последний элемент. Это полезно для поведения по умолчанию / переопределения, но может быть проблематичным, если порядок не учитывается.
const c = objs.reduce(concatenate, {});
const concatenate = (a, o) => ({...a, ...o});
console.log(
'concatenation',
c,
`enumerable keys: ${ Object.keys(c) }`
);
// concatenation { a: 'a', b: 'cb', c: 'c' } enumerable keys: a,b,c
Делегирование
Делегирование - это когда объект пересылает или делегирует другому объекту.
Примеры
Встроенные типы JavaScript используют делегирование для отсылки к встроенным методам, вызывающим цепочку прототипов. Например, [] .map () делегирует Array.prototype.map (), obj.hasOwnProperty () делегирует Object.prototype.hasOwnProperty () и так далее.
Плагины jQuery полагаются на делегирование для совместного использования встроенных методов и плагинов всеми экземплярами jQuery.
«masters» Sketchpad были динамическими делегатами. Модификации делегата будут мгновенно отображены во всех объектных экземплярах.
Photoshop использует делегаты, называемые «умные объекты», для обращения к изображениям и ресурсам, определенным в отдельных файлах. Изменения в объекте, на который ссылаются «умные объекты», отражаются во всех экземплярах «умных объектов».
Когда использовать
-
Сохранение памяти: каждый раз, когда у вас может быть потенциально много экземпляров, было бы полезно использовать одинаковые свойства или методы для каждого экземпляра, которые в противном случае требовали бы выделения большего объема памяти.
-
Динамическое обновление многих экземпляров: каждый раз, когда множество экземпляров объекта должны иметь одинаковое состояние, которое вам придется обновлять динамически и мгновенно изменять в каждом случае, например, Sketchpad «мастера» или «умные объекты» в Photoshop.
Соображения
Делегирование обычно используется для имитации наследования классов в JavaScript (связано с ключевым словом extends), но требуется очень редко.
Делегирование может использоваться, чтобы точно имитировать поведение и ограничения наследования классов. На самом деле, наследование классов в JavaScript построено поверх статических делегатов через цепочку делегирования прототипа. Старайтесь Избегать is-a образа мышления.
Свойства делегата не перечислимы с использованием общих механизмов, таких как Object.keys (instanceObj).
Делегирование экономит память за счет производительности поиска свойств, а также отключения некоторых оптимизаций JS-движка, которые отключены для динамических делегатов (делегатов, которые меняются после их создания). Однако даже в самом медленном случае производительность поиска свойств измеряется в миллионах операций в секунду. Возможно, это тонкости не для вас, если вы не создаете библиотеку служебных программ для операций с объектами или графического программирования, например, RxJS или three.js.
Необходимо различать состояние экземпляра и состояние делегата.
Шаренное состояние динамических делегатов не является безопасным для экземпляра. Изменения распространяются на все экземпляры. Общее состояние динамических делегатов обычно (но не всегда) является ошибкой.
Классы ES6 не создают динамических делегатов в ES6. Возможно, они работают в babel, но в реальных средах ES6 они не справятся.
Примеры кода
const delegate = (a, b) => Object.assign(Object.create(a), b);
const d = objs.reduceRight(delegate, {});
console.log(
'delegation',
d,
`enumerable keys: ${ Object.keys(d) }`
);
// delegation { a: 'a', b: 'ab' } enumerable keys: a,b
console.log(d.b, d.c); // ab c
Заключение
Мы поняли:
- Все объекты, созданные из других объектов и языковых примитивов, являются композитными объектами.
- Акт создания композитного объекта известен как композиция.
- Существуют разные типы объектов.
- Соотношения и зависимости, которые мы формируем при композиции объектов, различаются в зависимости от того, как собраны объекты.
- Is-a отношения (вид, образованный классовым наследованием) являются самой узкой формой отношений в OOП и их, как правило, следует избегать, в случаях, когда это практично.
- «Банда четырех» предупреждает что создание объектов путем сборки более мелких объектов, для формирования большого целого лучше, чем наследование от монолитного базового класса или базового объекта. «Выбирайте композицию объекта вместо наследования класса».
- Агрегация объединяет объекты в перечислимые коллекции, где каждый член коллекции сохраняет свою собственную идентификацию, например массивы, деревья DOM и т. д.
- Делегирование создает объекты, связывая цепочки делегирования, где объект пересылает или передает свойства для поиска у другого объекта. например, [].map() делегирует в Array.prototype.map ()
- Конкатенация объединяет объекты, расширяя существующий объект новыми свойствами, например Object.assign (destination, a, b), {... a, ... b}.
- Определения различных видов композиций объектов не являются взаимоисключающими. Делегирование является подмножеством агрегации, объединение может быть использовано для формирования делегатов и агрегатов и т. д.
Это не единственные три вида объектной композиции. Также возможно сформировать свободные динамические отношения между объектами через отношения знакомства/ассоциации, где объекты передаются как параметры другим объектам (инжекция зависимостей) и т. д.
Вся разработка программного обеспечения - композиция. Существуют простые, гибкие способы создания объектов и хрупкие, ломкие способы. Некоторые формы объектной композиции образуют слабосвязанные отношения между объектами, а другие образуют очень плотную связь.
Ищите способы композиции, где небольшое изменение требований к программе потребует лишь небольшого изменения в реализации кода. Выражайте свое намерение четко и кратко и помните: если вы считаете, что вам нужно классовое наследование, велики шансы, что есть способ сделать это лучшим образом.
Оригинал статьи Eric Elliott The Hidden Treasures of Object Composition
Подписывайтесь на наш канал в Telegram Front End Dev и получайте больше полезной и интересной информации!