Недостаток композиции объектов перед наследованием классов - PullRequest
13 голосов
/ 20 октября 2010

В большинстве книг по патентам о дизайне говорится, что мы должны «отдавать предпочтение композиции объектов, а не наследованию классов».

Но может ли кто-нибудь привести пример, что наследование лучше, чем композиция объектов.

Ответы [ 8 ]

12 голосов
/ 20 октября 2010

Наследование подходит для is-a отношений. Это плохо подходит для has-a отношений.

Поскольку большинство отношений между классами / компонентами попадают в сегмент has-a (например, класс Car, скорее всего, не является HashMap, но он может иметь HashMap), тогда он следует часто лучшая идея для моделирования отношений между классами, а не наследования.

Это не означает, однако, что наследование бесполезно или не является правильным решением для некоторых сценариев.

7 голосов
/ 20 октября 2010

Мой простой ответ заключается в том, что вы должны использовать наследование для поведенческих целей.Подклассы должны переопределять методы, чтобы изменить поведение метода и самого объекта.

В этой статье (интервью с Эрихом Гаммой, одним из GoF) ясно объясняется, почему Композиция объекта Favor вместо наследования класса .

4 голосов
/ 20 октября 2010

В Java всякий раз, когда вы наследуете от класса, ваш новый класс также автоматически становится подтипом исходного типа класса.Поскольку это подтип, он должен придерживаться принципа замены Лискова .
Этот принцип в основном говорит о том, что вы должны иметь возможность использовать подтип в любом месте, где ожидается супертип.Это сильно ограничивает то, как поведение вашего нового унаследованного класса может отличаться от исходного класса.
Ни один компилятор не сможет заставить вас придерживаться этого принципа, но у вас могут возникнуть проблемы, если вы этого не сделаете, особенно когда другиепрограммисты используют ваши классы.

В языках, которые допускают создание подклассов без подтипов (например, CZ язык ), правило «Компоновка объектов в пользу наследования» не так важно, как в таких языках, как Javaили C #.

2 голосов
/ 20 октября 2010

Наследование позволяет использовать объект производного типа практически в любых обстоятельствах, когда можно использовать объект базового типа. Композиция не позволяет этого. Используйте наследование, когда такая замена требуется, и композицию, когда это не так.

0 голосов
/ 29 августа 2018

Это отличный вопрос. Один я просил в течение многих лет, на конференциях, в видео, в блогах. Я слышал всевозможные ответы. Единственный хороший ответ, который я услышал, это предварительное исполнение:

Различия в производительности в языках. Иногда классы используют преимущества встроенной оптимизации движка, а динамические композиции - нет. В большинстве случаев это гораздо меньше, чем проблемы, связанные с наследованием классов, и обычно вы можете встроить все, что вам нужно для оптимизации производительности, в один класс, обернуть вокруг него заводскую функцию и получить необходимые преимущества без проблемная иерархия классов.

Вы никогда не должны беспокоиться об этом, если не обнаружите проблему. Затем вы должны профилировать и тестировать различия в perf, чтобы сделать обоснованные компромиссы по мере необходимости. Зачастую существуют другие способы оптимизации производительности, которые не включают наследование классов, включая такие приемы, как встраивание, делегирование методов, запоминание чистых функций и т. Д. Perf зависит от конкретного приложения и языкового движка. Профилирование здесь важно.

Кроме того, я слышал много распространенных заблуждений. Наиболее распространенным является путаница с системами типов:

Сопоставление типов с классами (есть несколько существующих ответов, сосредоточенных на этом уже здесь). Композиции могут удовлетворять требованиям полиморфизма путем реализации интерфейсов. Классы и типы являются ортогональными, хотя в большинстве языков, поддерживающих классы, подклассы автоматически реализуют интерфейс суперкласса, поэтому это может показаться удобным.

Есть три очень веских причины, чтобы избежать наследования классов, и рост снова и снова:

Проблема гориллы / банана

"Я думаю, что отсутствие возможности повторного использования возникает в объектно-ориентированных языках, а не в функциональных языках. Поскольку проблема с объектно-ориентированными языками заключается в том, что у них есть вся эта неявная среда, которую они носят с собой. Вы хотели банан, но то, что ты получил, было гориллой, держащей банан и целые джунгли. " ~ Джо Армстронг, цитата из "Кодеров в работе" Питера Сейбела.

Эта проблема в основном связана с отсутствием выборочного повторного использования кода в наследовании классов. Композиция позволяет вам выбирать только те части, которые вам нужны, подходя к дизайну программного обеспечения из подхода «маленьких, многократно используемых деталей», а не создавая монолитные конструкции, которые инкапсулируют все, что связано с определенной функциональностью.

Хрупкая проблема базового класса

Наследование классов - это самая тесная связь, доступная в объектно-ориентированном проектировании, поскольку базовый класс становится частью реализации дочерних классов. Вот почему вы также услышите совет классического «Дизайнерского паттерна» от Gang of Four: «Программируйте интерфейс, а не реализацию».

Проблема с наследованием реализации заключается в том, что даже малейшее изменение внутренних деталей этой реализации может потенциально сломать дочерние классы. Если интерфейс является общедоступным и каким-либо образом доступным для пользователя, это может привести к нарушению кода, о котором вы даже не знаете.

Это причина того, что иерархии классов становятся хрупкими - их трудно изменить по мере их роста с новыми вариантами использования.

Обычный рефрен заключается в том, что мы должны постоянно рефакторинг нашего кода (см. Martin Fowler и др. По экстремальному программированию, гибкому программированию и т. Д.). Ключом к успеху рефакторинга является то, что вы не можете ломать вещи, но, как мы только что видели, трудно реорганизовать иерархию классов, не ломая вещи.

Причина в том, что невозможно создать правильную иерархию классов, не зная всего, что вам нужно знать о сценариях использования, но вы не можете этого знать в развивающемся программном обеспечении. Варианты использования постоянно добавляются или изменяются в проектах.

Существует также процесс обнаружения в программировании, где вы обнаруживаете правильный дизайн при реализации кода и узнаете больше о том, что работает, а что нет.Но с наследованием классов, как только вы начнете таксономию классов, вы загнали себя в угол.

Вам нужно знать информацию, прежде чем начинать реализацию, но часть изучения информации, которая вам нужна, включает построениереализация.Это ловушка-22.

Дублирование по необходимости. Вот где действительно начинается смертельная спираль.Иногда вам действительно нужен банан, а не горилла, держащая банан, и целые джунгли.Таким образом, вы копируете и вставляете это.Теперь есть ошибка в банане, так что вы это исправите.Позже вы получите такой же отчет об ошибке и закроете его.«Я уже исправил это».И тогда вы получите тот же отчет об ошибке снова.И опять.Ой-ой.Это не исправлено.Вы забыли другой банан!Google "скопируйте макароны".

В других случаях вам действительно нужно добавить новый вариант использования в свое программное обеспечение, но вы не можете изменить исходный базовый класс, поэтому вместо этого вы копируете и вставляете весь классиерархия в новую и переименуйте все классы, которые вам нужны в иерархии, чтобы включить этот новый вариант использования в базу кодаСпустя 6 месяцев новый разработчик просматривает код и задается вопросом, от какой иерархии классов унаследовать, и никто не может дать хороший ответ.

Дублирование по необходимости приводит к копированию беспорядка пасты, и довольно скоро люди начинают разбрасываться вокругСлово «переписать», как будто это не имеет большого значения.Проблема заключается в том, что большинство проектов переписать не удастся.Я могу назвать несколько организаций, которые в настоящее время поддерживают две команды разработчиков вместо одной, пока они работают над проектом переписывания.Я видел, как такие организации урезали финансирование одному или другому, и я видел, как такие проекты жуют столько денег, что у стартапа или малого бизнеса заканчиваются деньги и он закрывается.

Разработчики недооцениваютвлияние наследования классов все время.Это важный выбор, и вам нужно знать о компромиссах, которые вы выбираете при каждом создании или наследовании базового класса.

0 голосов
/ 21 октября 2010

Наследование необходимо для подтипа.Обратите внимание:

class Base {
    void Foo() { /* ... */ }
    void Bar() { /* ... */ }
}

class Composed {
    void Foo() { mBase.Foo(); }
    void Bar() { mBase.Foo(); }
    private Base mBase;
}

Несмотря на то, что Composed поддерживает все методы Foo, его нельзя передать функции, которая ожидает значение типа Foo:

void TakeBase(Base b) { /* ... */ }

TakeBase(new Composed()); // ERROR

Итак, если вам нужен полиморфизм, вам нужно наследование (или его реализация интерфейса двоюродного брата).

0 голосов
/ 20 октября 2010

Это основополагающий принцип хорошего OOD.Вы можете назначить поведение классу динамически «во время выполнения», если вы используете композицию в своем дизайне, а не наследование, как в шаблоне стратегии.Скажем,

interface Xable {
doSomething();
}

class Aable implements Xable { doSomething() { /* behave like A */ } }
class Bable implements Xable { doSomething() { /* behave like B */ } }

class Bar {
Xable ability;

public void setAbility(XAble a) { ability = a; }

public void behave() { 
ability.doSomething();
}

}
/*now we can set our ability in runtime dynamicly */

/*somewhere in your code */

Bar bar = new Bar();
bar.setAbility( new Aable() );
bar.behave(); /* behaves like A*/
bar.setAbility( new Bable() );
bar.behave(); /* behaves like B*/

, если бы вы использовали наследование, «Бар» получал бы поведение «статически» по сравнению с наследованием.

0 голосов
/ 20 октября 2010

Просто представьте, что у вас есть "is-a" или "has-a" отношения

В этом примере Human "is-a" Animal, и он может наследовать разные данные из класса Animal. Поэтому наследование используется:

abstract class Animal {
    private String name;
    public String getName(){
        return name;
    }
    abstract int getLegCount();
}

class Dog extends Animal{
    public int getLegCount(){
        return 4;
    }
}

class Human extends Animal{
    public int getLegCount(){
        return 2;
    }
}

Композиция имеет смысл, если один объект является владельцем другого объекта. Как объект «Человек», владеющий объектом «Собака». Итак, в следующем примере объект Human "has-a" объект Dog

class Dog{
    private String name;
}
class Human{
    private Dog pet;
}

надеюсь, что помогло ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...