Почему композиция не нарушает Принцип единственной обязанности?

1,00
р.
Я решаю задачу о нахождении лидера (leader election).
Задача чисто алгоритмическая. Есть 2 формы задачи. У меня есть абстрактный класс для представления данных и абстрактный класс Solver. Для каждой формы задачи эти классы я расширяю в соответствии с нуждами этой формы задачи. То есть, решая задачу для первой формы мне нужно написать так в клиентском коде:
MyData data = new MyDataForm1() MySolver solver = new MySolverForm1()
Это можно было бы скомпозировать так:
public abstract class AbstractTask{ // some code }
public class Task1{ MyData data = new MyDataForm1() MySolver solver = new MySolverForm1()
/ome code }
public class Task2{ MyData data = new MyDataForm2() MySolver solver = new MySolverForm2()
/ome code }
Тогда в клиентском коде для первой формы, например, можно будет делать так:
AbstractTask task = new Task1()
Это же композиция? Но тут такая проблема: Получается что у классов Task 2 обязанности. Они и данные хранят и задачу решают (да, делегируя это экземплярам MyData и MySolver, но все же). И так ведь получается всегда при композиции. Мы включаем экземпляры нескольких классов в один класс в качестве полей. У включенных классов были какие то обязанности. Значит у включающего класса будет несколько обязанностей.
Я понимаю тут так, что это более высокий уровень абстракции. Например, на уровне отдельных деталей машины можно рассматривать мотор, магнитолу и руль как отдельные классы, которые выполняют какую-то обязанности. Но потом мы начинаем рассматривать машину. Она состоит из этих частей. Но теперь мы рассматриваем на более высоком уровне абстракции (нам просто надо ездить), поэтому можно сказать что класс Машина все-таки выполняют всего одну задачу (езда).

Ответ
Я понимаю тут так, что это более высокий уровень абстракции. Например, на уровне отдельных деталей машины можно рассматривать мотор, магнитолу и руль как отдельные классы, которые выполняют какую-то обязанности. Но потом мы начинаем рассматривать машину. Она состоит из этих частей. Но теперь мы рассматриваем на более высоком уровне абстракции (нам просто надо ездить), поэтому можно сказать что класс Машина все-таки выполняют всего одну задачу (езда).
Вы здесь сами верно ответили на вопрос. Дело в том, что у нового класса будет обязанность, отличная от обязанностей объектов, которые он в себя включает. Если говорить конкретно, то обязанность нового класса заключается в том, чтобы соединять вместе данные и алгоритм решения. В последующем может обнаружиться, что такой объект будет весьма полезен, если понадобятся промежуточные шаги. Эта обязанность звучит несколько абстрактно, но тем не менее имеет право на жизнь. И, как можно заметить, она более высокоуровневая, чем обязанности включаемых классов. Об этом я вскользь упоминал в предыдущем ответе на ваш вопрос об SRP.
Можно вывести правило: чем выше уровень "модуля", тем более высокоуровневыми являются его обязанности.
Возьмем тип float. Его обязанность -- реализовывать работу с числами с плавающей точкой (представление, плюс базовые математические операции).
Используя тип float (т.е. используя композицию), вы пишете функцию расчета гипотенузы по заданным катетам. Обязанность этой функции -- считать гипотенузу.
Дальше вы включаете эту функцию в пакет PlanimetryAlgorithms. Обязанность этого пакета -- предоставлять различные алгоритмы, связанных с планиметрий (т.е. геометрией фигур на плоскости).
Пакет PlanimetryAlgorithms может, в свою очередь, входить в библиотеку Geometry, куда также будут входить пакеты для других разделов геометрии. Обязанность этой библиотеки -- предоставлять различные функции, касающиеся геометрии вообще.
Как видно, с повышением гранулярности обязанность становится более высокоуровневой, но при этом ее единственность соблюдается. Функция расчета гипотенузы не выдает нам заодно значения всех углов в этом треугольнике, пакет PlanimetryAlgorithms не содержит методов для рисования фигур, а библиотека Geometry не начинает вдруг заниматься физикой.