Композиция программного обеспечения: Вступление

Композиция — это составление целого из частей.
На моём первом уроке программирования в старших классах мне рассказывали, что программирование — это разделение сложной задачи на несколько небольших, и объединение простых результатов для получения конечного решения сложной проблемы.
Одним из моих главных сожалений в жизни является то, что я не смог рано понять значение того урока. Я узнал суть проектирования программного обеспечения слишком поздно.
Я проводил собеседования с сотнями разработчиков. Из этих встреч я узнал, что я не одинок. Очень немногие разработчики имели хорошее понимание сути разработки программ. Большинство не знали о самых важных инструментах, которые имеются в наличии, или как их использовать. 100% пытались ответить на один или на оба самых важных вопроса в области разработки программного обеспечения:
- Что такое композиция функций?
- Что такое композиция объектов?
Проблема заключается в том, что вы не можете избежать композиции, только потому что не знаете о ней. Вы все-равно делаете это — но делаете плохо. Вы пишете код с большим количеством ошибок и делаете его сложным для понимания другим разработчикам и это большая проблема. Мы тратим больше времени на поддержку программного обеспечения, чем на создание его с нуля, и наши ошибки влияют на миллиарды людей по всему миру.
Во всем мире используют различное программное обеспечение. Каждый автомобиль это мини-суперкомпьютер на колесах, и проблемы с разработкой его программного обеспечения может создают реальные проблемы и стоят человеческих жизней. В 2013 году комитет признал команду разработчиков программного обеспечения Toyota виновной в грубом нарушении после того, как расследование аварии выявило спагетти-код с 10000 глобальными переменными.
Хакеры и правительственные агенты собирают ошибки, чтобы шпионить за людьми, красть кредитные карты, использовать вычислительные ресурсы для запуска распределенных атак типа “отказ в обслуживании” (DDoS), взламывать пароли и даже манипулировать выборами.
Мы должны добиваться лучшего.
Вы составляете программы каждый день
Если вы разработчик, вы составляете функции и структуры данных каждый день, знаете вы это или нет. Вы можете делать это сознательно (что лучше), или случайно, используя клейкую ленту и суперклей.
Процесс разработки это разделение больших проблем на более мелкие, создание компонентов, которые решают эти маленькие проблемы, и затем составление всех этих частей вместе, чтобы получить конечное приложение.
Компонуем функции
Композиция функций — это процесс применения одной функции к результату другой. Например, в математике даны две функции f и g , в результате будет будет (f ∘ g)(x) = f(g(x)) , где кругляшок является оператором композиции. Часто говорят "применение к" или "после". Вы можете произнести вслух "применение f к g равно результату f от g от x " или " f применяется после g от x ". Мы говорим f после g , потому что g выполняется первой, а результат её выполнения является аргументом для f .
Всякий раз когда вы пишите подобный код, вы компонуете функции:
Всякий раз когда вы пишите цепочку промисов, вы компонуете функции:
Точно так же, каждый раз, когда вы делаете цепочку вызовов методов массива, методов lodash, observables (RxJS, и т.д.), вы компонуете функции. Если вы используете цепочки вызовов, вы компонуете функции. Если вы передаете возвращаемые значения в другие функции, вы компонуете функции. Если вы последовательно вызываете два метода вы компонуете их, используя this в качестве входных данных.
Если вы используете цепочки вызовов, вы компонуете функции.
Когда вы используете композицию функций намеренно, то делаете это лучше.
Специально скомпоновав функции, мы можем улучшить наш doStuff() до одной строчки.
Главное замечание к такой форме написания заключается в том, что её сложнее отлаживать. Например как бы вы написали следующий код, используя композицию функций?
Во-первых, давайте вынесем логирование “after f”, “after g” в отдельную вспомогательну функцию с именем trace() :
Теперь мы можем использовать её:
Популярные библиотеки функционального программирования, такие как Lodash и Ramda, уже включают в себя утилиты для упрощения композиции функций. Вы можете переписать функцию выше, следующим образом:
Если хотите попробовать этот код без импорта чего-либо, то можете определить функцию pipe таким образом:
Не беспокойтесь, если не смогли еще уследить, как это работает. Позже мы рассмотрим функциональную композицию более подробно. На самом деле это настолько важно, потому вы увидите ее определение и демонстрацию множество раз в этом тексте. Цель в том, чтобы помочь вам понять настолько, чтобы сделать использование этого автоматическим. Будьте едины с композицией.
pipe() создает конвейер из функций, где результат одной функции является аргументом для следующей. Когда вы используете pipe() (или его аналог compose() ), то не используете промежуточные переменные. Описание функций без указания аргументов называется бесточечной нотацией. Для этого вы вызываете функцию, которая возвращает новую функцию, без явного её объявления. Это означает, что вам не нужно использовать ключевое слово function или стрелочный синтаксис ( => ).
Бесточечную нотацию можно использовать гораздо дальше, и это хорошо, потому то промежуточные переменные создают ненужную сложность вашим функциям.
Несколько преимуществ уменьшения сложности:
Работа памяти
Средний человеческий мозг имеет только несколько общих ресурсов для дискретных квантов в рабочей памяти, и каждая переменная потенциально потребляет один из этих квантов. Когда вы добавляете больше переменных, то наша способность точно вспомнить значение каждой переменной уменьшается. Модель рабочей памяти включают 4–7 дискретных квантов, превысив эти значения коэффициент ошибок резко возрастают.
Используя pipe , мы исключили 3 переменных, освободив тем самым почти половину нашей доступного запаса памяти для других вещей. Это значительно снижает умственную нагрузку. Разработчики, как правило, лучше разбивают информацию в памяти, чем средний человек, но не настолько, чтобы забыть о её сохранении.
Отношение сигнал/шум
Короткий код увеличивает отношение сигнал/шум в вашем коде. Это как слушать радио — когда оно не настроено должным образом, вы будете получать множество помех, из-за чего труднее услышать музыку. Но стоит настроить на правильную станцию, как шум уходит и вы получаете отличный музыкальный сигнал.
Писать код — это тоже самое, короткое выражение ведет к лучшему пониманию. Какой-то код несет полезную информацию, а какой-то просто занимает место. Если вы сможете уменьшить объем кода, не уменьшая его значение, которое передается, то облегчите анализ и понимание кода для других людей, которым нужно его прочитать.
Место для ошибок
Взгляните на функции до и после. Похоже, что эта функция пошла на диету и похудела. Это важно, потому что дополнительный код создает дополнительное место для ошибок, что означает больше ошибок будут скрываться в нем.
Меньше кода = меньше места для ошибок = меньше ошибок
Компонуем объекты
“Предпочитайте композицию наследованию класса” — «Банда Четырех», «Приёмы объектно-ориентированного проектирования. Паттерны проектирования» (прим. пер. the Gang of Four, “Design Patterns: Elements of Reusable Object Oriented Software”)
“В информатике составные или сложные типы данных — это тип, который может быть получен в программе, используя примитивные типы языка программирования и другие составные типы. […] Построение составного типа является композицией.”
А это составной тип
Кроме того, все массивы, Sets, Maps, WeakMaps, TypedArrays и т. д. являются составными типами данных. Всякий раз когда вы создаете любую структуру данных, не являющейся примитивом, вы производите композицию в каком-то роде.
Обратите внимание, что “Банда Четырех” определяет паттерн, называемый “Компоновщик”, который является особым типом рекурсивной композиции объектов, что позволяет одинаково обрабатывать отдельные компоненты и агрегированные составные типы. Некоторые разработчики путаются, думая, что паттерн компоновщик единственное определение композиции объектов. Не дайте себя запутать, существует множество различных типов композиции объектов.
“Банда Четырех” продолжает, — “вы увидите как композиция объектов используется снова и снова в паттернах проектирования”, и затем они показывают три вида связей между скомпонованным объектами, в которые включены делегирование (используется в паттернах состояние, стратегия и посетитель), осведомленность (когда объекту известно о другом объекте посредством ссылки, обычно переданной как параметр: Uses-A отношение, например в обработчик запроса можно передать ссылку на логгер, который будет выводить запрос — запрос использует логгер), и агрегирование (когда дочерние объекты являются частью родительского: Has-a отношение, например дочерние DOM элементы являются составными частями DOM-узла — у DOM-узла имеются дети).
Наследование классов может использоваться для создания составных объектов, но это ограниченный и хрупкий способ делать это. Когда Банда Четырех говорит “преподчитайте композицию объектов наследованию”, они советуют вам использовать гибкие подходы для построения составных объектов, а не жесткий, тесно связанный подход наследования классов.
Мы будем использовать более общее определение композиции объектов из книги “Categorical Methods in Computer Science: With Aspects from Topology” (1989):
Составные объекты формируются путем объединения объектов таким образом, что каждый из них является “частью” первого.
Еще одна хорошая отсылка — “Reliable Software Through Composite Design”, Glenford J Myers, 1975. Обе книги давно вышли из печати, но вы все еще можете найти продавцов на Amazon или eBay, если вы хотите глубже изучить тему композиции объектов с технической точки зрения.
Наследование классов — это только один из видов построения составного объекта. Все классы дают в результате составные объекты, но не все сложные объекты созданы классами или наследованием классов. “Предпочитайте композицию объектов наследованию” означает, что следует создавать составные объекты из мелких частей, а не наследовать все свойства от предка в иерархии классов. Последнее вызывает большое разнообразие известных проблем в объектно-ориентированном проектировании:
- Проблема тесной связи: поскольку дочерние классы зависят от реализации родительского класса, то наследование является самой тесной связью, доступной в объектно-ориентированном дизайне.
- Проблема хрупкого базового класса: из-за тесной связи изменения в базовом классе могут нарушить работу большого числа дочерних классов, и вероятно в коде, управляемом третьими сторонами. Автор может нарушить код, о котором он не знает.
- Проблема негибкой иерархии: с таксономией одного предка и учетом достаточного времени и эволюции, все таксономии классов в конечном итоге неверны для новых случаев их применения.
- Проблема дублирования по необходимости: из-за негибкой иерархии новые классы часто реализуются путем дублирования, а не c помощью расширения, что приводит к появлению подобных классов, которые неожиданно различаются. Как только происходит дублирование, становится неочевидно, из какого класса должны происходить новые классы или почему.
- Проблема гориллы с бананом: “…проблема в объектно-ориентированных языках заключается в том, что у них существует неявная среда, которую они с собой несут. Вы хотели всего лишь банан, но в результате получаете гориллу, держащую этот банан, и все джунгли в придачу.”
Наиболее распространенная форма объектной композиции в JavaScript известна как объединение объектов (или примесь). Это работает как мороженное, вы начинаете с объекта (допустим ванильное мороженное), и затем подмешиваете дополнительные функции, которые хотите. Добавьте орехи, карамель, шоколад, и в итоге получите орехово-карамельно-шоколадное мороженое.
Создание составных объектов через наследование классов:
Создание составных объектов через примеси:
Позже мы рассмотрим другие виды композиции объектов более подробно. На данный момент вы можете понять, что:
- Существует несколько способов, чтобы сделать композицию объектов.
- Какие-то способы лучше, чем другие.
- Вы хотите выбрать самое простое и гибкое решение для поставленной задачи.
Заключение
Эта статья не о функциональном программировании (ФП) против объектно-ориентированного (ООП), или один язык против другого. Компонентами могут быть функции, структуры данных, классы и т.д. Различные языки программирования, как правило, предоставляют разные базовые элементы для компонентов — Java предлагает классы, Haskell предлагает функции и т.д. Но независимо от того, какой язык и какую парадигму вы предпочитаете, вам никуда не деться от составления функций и структур данных. В конце концов, это то, к чему все сводится.
Мы будем много говорить о функциональном программировании, потому что функции в JavaScript очень просто компонуются, и сообщество функционального программирования вложило много времени и усилий для формирования техник композиции функций.
Чего мы не будем делать, так это говорить, что функциональное программирование лучше объектно-ориентированного программирования, или что вы должны выбрать одно вместо другого. ООП против ФП — неправильное противопоставление. Каждое реальное приложение на Javascript, которое я видел в последние годы, активно смешивает ФП и ООП.
Мы будем использовать композицию объектов, чтобы создавать типы данных для функционального программирования, а функциональное программирование — чтобы создавать объекты для ООП.
Независимо от того как вы пишите программное обеспечение, вы должно хорошо составить его.
Суть разработки программного обеспечения — это композиция.
Разработчик, который не понимает композицию, похож на строителя дома, который не знает о болтах или гвоздях. Построение программы без знания композиции похоже на устаноку стен с помощью клейкой ленты и клея.
Пришло время упростить и лучший для этого способ — добраться до сути. Проблема в том, что почти никто в отрасли не имеет хорошего знания сути. Мы, как отрасль, подвели вас, разработчиков. Наша обязанность как отрасли — это лучше обучать разработчиков. Мы должны совершенствоваться, должны взять на себя ответственность. На программном обеспечении работает все в мире, от экономики до медицинского оборудования. На этой планете нет буквально ни одного уголка, где обитает человек, на который бы не повлияло бы качество наших программ. Нам нужно осознавать, что мы делаем.
Теперь время изучить как составлять программы.
Узнайте больше на EricElliottJS.com
Видеоуроки по композиции функций и объектов доступны для пользоваетелей EricElliottJS.com. Если вы не еще не являетесь им, регистрируйтесь сегодня

Eric Elliott автор книг “Programming JavaScript Applications” (O’Reilly), и “Learn JavaScript with Eric Elliott”. Он внес свой вклад в опыт разработки программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, и ведущих артистов, в том числе Usher, Frank Ocean, Metallica, и множество других.
Он работает удаленно из любой точки мира с самой красивой женщиной в мире.
Что такое композиция в программировании
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Объектно-ориентированное программирование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.
- объектно-ориентированное программирование использует в качестве основных логических конструктивных элементов объекты, а не алгоритмы;
- каждый объект является экземпляром определенного класса
- классы образуют иерархии.
Программа считается объектно-ориентированной, только если выполнены все три указанных требования. В частности, программирование, не использующее наследование, называется не объектно-ориентированным, а программированием с помощью абстрактных типов данных.
Согласно парадигме ООП программа состоит из объектов, обменивающихся сообщениями. Объекты могут обладать состоянием, единственный способ изменить состояние объекта — послать ему сообщение, в ответ на которое, объект может изменить собственное состояние.
Основные принцыпы ООП.
- Инкапсуляция — сокрытие реализации.
- Наследование — создание новой сущности на базе уже существующей.
- Полиморфизм — возможность иметь разные формы для одной и той же сущности.
- Абстракция — набор общих характеристик.
- Посылка сообщений — форма связи, взаимодействия между сущностями.
- Переиспользование— все что перечислено выше работает на повторное использование кода.
Это единственно верный порядок парадигм ООП, так как каждая последующая использует предыдущие.
Что такое «инкапсуляция»?
Инкапсуляция – это свойство системы, позволяющее объединить данные и методы, работающие с ними, в классе и скрыть детали реализации от пользователя, открыв только то, что необходимо при последующем использовании.
Цель инкапсуляции — уйти от зависимости внешнего интерфейса класса (то, что могут использовать другие классы) от реализации. Чтобы малейшее изменение в классе не влекло за собой изменение внешнего поведения класса.
Что такое «наследование»?
Наследование – это свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью.
Класс, от которого производится наследование, называется предком, базовым или родительским. Новый класс – потомком, наследником или производным классом.
Полиморфизм – это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
Преимуществом полиморфизма является то, что он помогает снижать сложность программ, разрешая использование одного и того же интерфейса для задания единого набора действий. Выбор же конкретного действия, в зависимости от ситуации, возлагается на компилятор языка программирования. Отсюда следует ключевая особенность полиморфизма — использование объекта производного класса, вместо объекта базового (потомки могут изменять родительское поведение, даже если обращение к ним будет производиться по ссылке родительского типа).
Полиморфная переменная, это переменная, которая может принимать значения разных типов, а полиморфная функция, это функция у которой хотя бы один аргумент является полиморфной переменной. Выделяют два вида полиморфных функций:
- ad hoc, функция ведет себя по разному для разных типов аргументов (например, функция draw() — рисует по разному фигуры разных типов);
- параметрический, функция ведет себя одинаково для аргументов разных типов (например, функция add() — одинаково кладет в контейнер элементы разных типов).
Абстрагирование – это способ выделить набор общих характеристик объекта, исключая из рассмотрения частные и незначимые. Соответственно, абстракция – это набор всех таких характеристик.
Что представляет собой «обмен сообщениями»?
Объекты взаимодействуют, посылая и получая сообщения. Сообщение — это запрос на выполнение действия, дополненный набором аргументов, которые могут понадобиться при выполнении действия. В ООП посылка сообщения (вызов метода) — это единственный путь передать управление объекту. Если объект должен «отвечать» на это сообщение, то у него должна иметься соответствующий данному сообщению метод. Так же объекты, используя свои методы, могут и сами посылать сообщения другим объектам. Обмен сообщениями реализуется с помощью динамических вызовов, что приводит к чрезвычайно позднему связыванию (extreme late binding).
Расскажите про основные понятия ООП: «класс», «объект», «интерфейс».
Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (контракт).
С точки зрения программирования класс можно рассматривать как набор данных (полей, атрибутов, членов класса) и функций для работы с ними (методов).
С точки зрения структуры программы, класс является сложным типом данных.
Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом. Каждый объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе.
Интерфейс – это набор методов класса, доступных для использования. Интерфейсом класса будет являться набор всех его публичных методов в совокупности с набором публичных атрибутов. По сути, интерфейс специфицирует класс, чётко определяя все возможные действия над ним.
В чем заключаются преимущества и недостатки объектно-ориентированного подхода в программировании?
- Объектная модель вполне естественна, поскольку в первую очередь ориентирована на человеческое восприятие мира, а не на компьютерную реализацию.
- Классы позволяют проводить конструирование из полезных компонентов, обладающих простыми инструментами, что позволяет абстрагироваться от деталей реализации.
- Данные и операции над ними образуют определенную сущность, и они не разносятся по всей программе, как нередко бывает в случае процедурного программирования, а описываются вместе. Локализация кода и данных улучшает наглядность и удобство сопровождения программного обеспечения.
- Инкапсуляция позволяет привнести свойство модульности, что облегчает распараллеливание выполнения задачи между несколькими исполнителями и обновление версий отдельных компонентов.
- Возможность создавать расширяемые системы.
- Использование полиморфизма оказывается полезным при:
- Обработке разнородных структур данных. Программы могут работать, не различая вида объектов, что существенно упрощает код. Новые виды могут быть добавлены в любой момент.
- Изменении поведения во время исполнения. На этапе исполнения один объект может быть заменен другим, что позволяет легко, без изменения кода, адаптировать алгоритм в зависимости от того, какой используется объект.
- Реализации работы с наследниками. Алгоритмы можно обобщить настолько, что они уже смогут работать более чем с одним видом объектов.
- Возможности описать независимые от приложения части предметной области в виде набора универсальных классов, или фреймворка, который в дальнейшем будет расширен за счет добавления частей, специфичных для конкретного приложения.
- Сокращается время на разработку, которое может быть отдано другим задачам.
- Компоненты многоразового использования обычно содержат гораздо меньше ошибок, чем вновь разработанные, ведь они уже не раз подвергались проверке.
- Когда некий компонент используется сразу несколькими клиентами, улучшения, вносимые в его код, одновременно оказывают положительное влияние и на множество работающих с ним программ.
- Если программа опирается на стандартные компоненты, ее структура и пользовательский интерфейс становятся более унифицированными, что облегчает ее понимание и упрощает использование.
- В сложных иерархиях классов поля и методы обычно наследуются с разных уровней. И не всегда легко определить, какие поля и методы фактически относятся к данному классу.
- Код для обработки сообщения иногда «размазан» по многим методам (иначе говоря, обработка сообщения требует не одного, а многих методов, которые могут быть описаны в разных классах).
- Документирование классов — задача более трудная, чем это было в случае процедур и модулей. Поскольку любой метод может быть переопределен, в документации должно говориться не только о том, что делает данный метод, но и о том, в каком контексте он вызывается.
- Неэффективность и неэкономное распределения памяти на этапе выполнения (по причине издержек на динамическое связывание и проверки типов на этапе выполнения).
- Излишняя универсальность. Часто содержится больше методов, чем это реально необходимо текущей программе. А поскольку лишние методы не могут быть удалены, они становятся мертвым грузом.
Что подразумевают в плане принципов ООП выражения «является» и «имеет»?
«является» подразумевает наследование. «имеет» подразумевает ассоциацию (агрегацию или композицию).
В чем разница между композицией и агрегацией?
Ассоциация обозначает связь между объектами. Композиция и агрегация — частные случаи ассоциации «часть-целое».
Агрегация предполагает, что объекты связаны взаимоотношением «part-of» (часть). Композиция более строгий вариант агрегации. Дополнительно к требованию «part-of» накладывается условие, что экземпляр «части» может входить только в одно целое (или никуда не входить), в то время как в случае агрегации экземпляр «части» может входить в несколько целых.
Что такое статическое и динамическое связывание?
Присоединение вызова метода к телу метода называется связыванием. Если связывание проводится компилятором (компоновщиком) перед запуском программы, то оно называется статическим или ранним связыванием (early binding).
В свою очередь, позднее связывание (late binding) это связывание, проводимое непосредственно во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называют динамическим (dynamic) или связыванием на стадии выполнения (runtime binding). В языках, реализующих позднее связывание, должен существовать механизм определения фактического типа объекта во время работы программы, для вызова подходящего метода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова методов определяет его и вызывает соответствующее тело метода. Механизм позднего связывания зависит от конкретного языка, но нетрудно предположить, что для его реализации в объекты должна включаться какая-то дополнительная информация.
Для всех методов Java используется механизм позднего (динамического) связывания, если только метод не был объявлен как final (приватные методы являются final по умолчанию).
Агрегация и композиция
Всегда считал, что агрегация — это синоним композиции, однако наткнулся на блог в интернете, где приводятся отличия композиции от агрегации.
Мне это снесло крышу. Поясните, пожалуйста, плюсы/минусы и того и другого на небольших примерах. Как это влияет на расширяемость, тестируемость и т. д.

Существует несколько видов взаимодействия объектов, объединенных под общим понятием «Has-A Relationship» или «Part Of Relationship». Это отношение означает, что один объект является составной частью другого объекта.
Существует два подвида этого отношения: если один объект создает другой объект и время жизни «части» зависит от времени жизни целого, то это называется «композиция», если же один объект получает ссылку (указатель) на другой объект в процессе конструирования, то это уже агрегация.
Давайте рассмотрим пример из .NET Framework, чтобы увидеть, какие ограничения/последствия несут эти отношения: StringWriter + StringBuilder .
Класс StringWriter — это специализированная версия класса TextWriter , которые активно используются при сериализации и для получения текстового представления объектов.
Конкретно StringWriter создает строковое представление объекта или графа объектов и опирается на экземпляр StringBuilder в своей работе. Т.е. мы можем сказать, что ‘StringWriter HAS A StringBuilder’ или ‘StringBuilder is part of StringWriter’. Теперь давайте посмотрим, как решить, должен ли StringWriter получать экземпляр StringBuilder -а извне или создавать его?
С одной стороны, нам, как клиенту класса StringWriter часто бывает все равно, что именно используется внутри этого класса для получения строкового представления. Это значит, что с точки зрения простоты использования лучше, чтобы StringWriter создавал экземпляр StringBuilder -а самостоятельно.
Но, с другой стороны, конкретный объект StringWriter -а может отвечать лишь за получения части строкового представления, а другая часть строки может вычисляться другим способом. С этой точки зрения, лучше, чтобы StringWriter принимал экземлпяр StringBuilder -а в конструкторе. Это же справедливо и для высоконагруженных систем, в которых разумно использование пула объектов.
Поскольку StringWriter — это библиотечный класс, который должен поддерживать оба сценария, то у него есть перегруженные версии конструктора: один из них создает StringBuilder внутри, а другой — принимает его снаружи.
Другими словами, выбор между композицией и агрегацией основан на соблюдении балланса между различными требованиями в дизайне:
Композиция: объект A управляет временем жизни объекта B
Композиция позволяет скрыть отношение использования объектов от глаз клиента.
Делает API использования класса более простым и позволяет перейти от использования одного класса, к другому (например, StringWriter мог бы поменять реализацию и начать использовать другой тип, например CustomStringBuilder ).
- Отношение достаточно жесткое, поскольку один объект должен уметь создавать другой: он должен знать конкретный тип и иметь доступ к функции создания. Композиция не позволяет использовать интерфейсы (без привлечения фабрик) и требует, чтобы класс имел доступ к конструктору другого класса: представьте, что конструктор StringBuilder -а является внутренним или же это интерфейс IStringBuilder и только клиенский код знает, какой экземпляр должен использоваться здесь и сейчас.
Агрегация: объект А получает ссылку на объект B
Более слабая связанность между объектом и его клиентом. Теперь мы можем использовать интерфейсы и одному объекту не нужно знать, как именно создавать другой объект.
Большая гибкость. Вытекает из первого пункта
Выставление наружу деталей реализации. Поскольку клиент класса должен предоставить зависимость в момент создания объекта (передать экземпляр StringBuilder -а в момент создания StringWriter -а, то сам факт этого отношения становится известен клиенту.
Из первого пункта вытекает увеличение сложности в работе клиентов, а также большая «жесткость» решения в долгосрочной перспективе. Теперь автор класса TextWriter уже не может принять решение самостоятельно и перейти от StringBuilder -а к чему-то другому. Да, можно «добавить еще один уровень абстракции» и выделить интерфейс IStringBuilder , но разорвать это отношение совсем будет невозможно без поломки всех существующих клиентов.
В заключении: дизайн — это поиск компромисса между различными факторами. Композиция проще с точки зрения клиентов класса, но налагает определенные ограничения: «целое» должно уметь создавать «составную часть». Агрегация гибче, но налагает другие ограничения: теперь «целое» не скрывает о существовании «составной части», а значит и не сможет заменить ее на другую «составную часть» в будущем.