Что такое композиция (Composition) в объектно-ориентированном программировании?

Ответ

Композиция — это принцип проектирования «has-a» («имеет»), при котором один класс (целое) включает в себя один или несколько объектов других классов (части) как часть своей внутренней структуры. Жизненный цикл части жестко привязан к жизненному циклу целого: часть создается и уничтожается вместе с целым.

Почему композиция предпочтительнее наследования? Принцип «Favor composition over inheritance» (предпочитай композицию наследованию)提倡使用组合而非继承,因为组合 обеспечивает:

  • Гибкость: Поведение можно изменять динамически во время выполнения, подменяя компоненты.
  • Слабое зацепление: Классы зависят от интерфейсов, а не от конкретных реализаций.
  • Избегание хрупкости: Изменения в иерархии наследования могут сломать подклассы.

Пример композиции (Java):

// Классы-компоненты
class Engine {
    void start() { System.out.println("Engine starts..."); }
}

class StereoSystem {
    void playMusic() { System.out.println("Playing music..."); }
}

// Класс-композит, использующий композицию
class Car {
    // Композиция: Car HAS-A Engine и HAS-A StereoSystem
    private final Engine engine;
    private final StereoSystem stereo;

    public Car() {
        // Части создаются внутри конструктора целого
        this.engine = new Engine();
        this.stereo = new StereoSystem();
    }

    public void startCar() {
        engine.start(); // Делегирование ответственности компоненту
        stereo.playMusic();
    }
}

// Использование
Car myCar = new Car();
myCar.startCar(); // Engine starts... Playing music...
// Когда 'myCar' будет уничтожен, его Engine и StereoSystem также перестанут существовать.

Композиция vs. Агрегация: Оба являются ассоциациями «has-a», но при агрегации часть может существовать независимо от целого (например, Driver в Car). Композиция — более сильная связь.

Ответ 18+ 🔞

Слушай, ну вот смотри, опять эти ваши принципы проектирования. «Композиция», «наследование»... Звучит, как будто речь про наследство бабушкиной квартиры, а не про код. Но ладно, разбираемся.

Вот представь, блядь, у тебя есть класс Car. И ты такой думаешь: «О, машина! Значит, это частный случай Vehicle! Наследуюсь, и все дела!». И вот ты уже, сука, в этой иерархии по уши, а потом бац — тебе надо сделать FlyingCar. И ты уже наследуешься и от Car, и от Plane, а в итоге у тебя получается пиздопроебибна, которая и едет криво, и летает через жопу, потому что у Plane в родителях метод move() переопределён для воздуха, а у Car — для асфальта. И ты сидишь и думаешь: «Ну я же просто хотел машину с пропеллером, ёпта!».

А теперь смотри на композицию. Это как конструктор «Лего», в рот меня чих-пых! Ты не пытаешься переделать один кубик в другой. Ты берёшь кубик Engine, берёшь кубик StereoSystem, и хуяк — собираешь из них целый класс Car. Машина имеет (has-a) мотор. Машина имеет магнитолу. Всё, логика проще некуда.

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

А почему это круче наследования? Да потому что гибкость, блядь! Захотел машину с электромотором — не лезешь в дебри переписывания родительского класса, рискуя всё сломать. Ты просто, сука, в момент создания машины подсовываешь ей другой объект — ElectricEngine вместо PetrolEngine. И всё! Машина даже не в курсе, что её начинку поменяли, она просто вызывает engine.start(). Это и есть слабая связанность, когда классы договариваются между собой через интерфейсы, а не через родственные связи, которые, как известно, только портят отношения.

Ну и чтобы не быть голословным, вот тебе живой пример, смотри:

// Вот два независимых кубика-компонента
class Engine {
    void start() { System.out.println("Двигатель заурчал..."); }
}

class StereoSystem {
    void playMusic() { System.out.println("Шансон пошёл..."); }
}

// А вот наш главный сборщик — класс Car.
class Car {
    // Композиция в чистом виде: Car В СЕБЕ СОДЕРЖИТ Engine и StereoSystem.
    // Ключевое слово `final` тут — как гвоздём прибито: эти части неотъемлемы.
    private final Engine engine;
    private final StereoSystem stereo;

    public Car() {
        // Части рождаются прямо здесь, в недрах целого.
        this.engine = new Engine();
        this.stereo = new StereoSystem();
    }

    public void startCar() {
        engine.start();   // Делегируем работу мотору
        stereo.playMusic(); // Делегируем работу магнитоле
        System.out.println("Поехали, блядь!");
    }
}

// Использование
Car myLada = new Car();
myLada.startCar();
// На экран выведет:
// Двигатель заурчал...
// Шансон пошёл...
// Поехали, блядь!
// А когда `myLada` умрёт (выйдет из области видимости), её движок и магнитола умрут с ней — верные до конца.

И да, не путай с агрегацией, это другая история. Агрегация — это когда твой Car имеет Driver, но водитель-то, сука, самостоятельная единица! Он может выйти из этой машины и сесть в другую, и не помрёт, когда машину раздавят. А в композиции часть без целого — просто кусок металлолома, смысла не имеющий.

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