Возможно, вы только что заметили, как люди говорили это в последние несколько месяцев, но хорошим программистам это известно намного дольше. Я, конечно, говорил об этом где-то около десяти лет.
Суть концепции в том, что наследование сопряжено с большими концептуальными издержками. Когда вы используете наследование, то каждый вызов метода имеет неявную диспетчеризацию. Если у вас есть глубокие деревья наследования, или многократная отправка, или (что еще хуже) и то, и другое, то выяснение, куда будет отправляться конкретный метод в любом конкретном вызове, может стать королевской PITA. Это усложняет правильные рассуждения о коде и усложняет отладку.
Позвольте мне привести простой пример для иллюстрации. Предположим, что глубоко в дереве наследования кто-то назвал метод foo
. Затем приходит кто-то другой и добавляет foo
наверху дерева, но делает что-то другое. (Этот случай чаще встречается при множественном наследовании.) Теперь этот человек, работающий с корневым классом, сломал неясный дочерний класс и, вероятно, не осознает этого. Вы можете иметь 100% охват модульными тестами и не заметить эту поломку, потому что человек наверху не будет думать о тестировании дочернего класса, а тесты для дочернего класса не думают о тестировании новых методов, созданных наверху , (По общему признанию, есть способы написать модульные тесты, которые поймут это, но есть также случаи, когда вы не можете легко написать тесты таким образом.)
В отличие от этого, когда вы используете композицию, при каждом вызове обычно становится понятнее, на что вы отправляете вызов. (Хорошо, если вы используете инверсию управления, например, с внедрением зависимости, то выяснить, куда идет вызов, также может быть проблематично. Но обычно это проще выяснить.) Это облегчает рассуждение об этом. В качестве бонуса композиция приводит к отделению методов друг от друга. Приведенный выше пример не должен происходить там, потому что дочерний класс переместился бы в какой-то неясный компонент, и никогда не возникает вопрос о том, был ли вызов foo
предназначен для неясного компонента или основного объекта.
Теперь вы абсолютно правы в том, что наследование и композиция - это два совершенно разных инструмента, которые обслуживают два разных типа вещей. Конечно, наследование несет концептуальные накладные расходы, но когда это правильный инструмент для работы, оно несет меньше концептуальных накладных расходов, чем попытка не использовать его и делать вручную то, что оно делает для вас. Никто, кто знает, что они делают, не скажет, что вы никогда не должны использовать наследование. Но будьте уверены, что это правильно.
К сожалению, многие разработчики узнают об объектно-ориентированном программном обеспечении, узнают о наследовании, а затем стараются использовать свой новый топор как можно чаще. Это означает, что они пытаются использовать наследование там, где композиция была правильным инструментом. Надеемся, что со временем они будут лучше учиться, но часто этого не происходит, если после нескольких удаленных конечностей и т. Д. Сказать им заранее, что это плохая идея, приводит к ускорению процесса обучения и снижению травматизма.