Ассоциация, агрегация и композиция

Author: Tatyana Milkina

Большая часть классов приложения связаны между собой. В этом разделе рассмотрим  две классификации отношений между классами.

1. Отношения, основанные на иерархии классов

Давайте начнем с отношений, основанных на иерархии классов - это отношения IS-A и HAS-A.

Отношения между классами IS-A и HAS-A

1.1. IS-A отношения

В ООП принцип IS-A основан на наследовании классов или реализации интерфейсов.

Например, если класс HeavyBox наследует Box, мы говорим, что HeavyBox является Box (HeavyBox IS-A Box):

HeavyBox IA-A Box

class Box {
   private double width;
   private double height;
   private double depth;
   …
}

class HeavyBox extends Box {
   private int weight;
   …
} 

Или другой пример - класс Lorry расширяет класс Car:

Lorry IS-A Car

class Car {
   …
}

class Lorry extends Car {
   …
} 

В этом случае Lorry IS-A Car.

Другой пример - классы Person и Driver:

Driver IS-A Person

class Person {
   …
}

class Driver extends Person {
   …
} 

Driver IS-A Person.

То же самое относится и к реализации интерфейсов. Если класс Transport реализует интерфейс Moveable, то они находятся в отношении Transport IS-A Moveable:

Transport IS-A Moveable

interface Moveable {
   …
}

class Transport implements Moveable{
   …
} 

1.2. HAS-A отношения

HAS-A отношения основаны на использовании.

Например, класс Car содержит переменную типа Engine:

Car HAS-A Engine

class Car {
  Engine engine;
   …
}

class Engine{
   …
} 

Мы говорим Car HAS-A Engine.

Или Shop содержит массив Category:

Shop HAS-A Category

class Shop {
  Category [] categories;
   …
}

class Category {
   …
} 

Это тоже отношение HAS-A: Shop HAS-A Category.

2. Более детализированная классификация взаимодействий

Выделяют также следующие взаимоотношения между классами - ассоциация, агрегация и композиция.

Ассоциация, агрегация и композиция

2.1. Ассоциация

Начнем с ассоциации. В этих отношениях объекты двух классов могут ссылаться друг на друга. Ассоциация может быть односторонней или двусторонней.

Например, класс Person содержит переменную Car, в этом случае мы можем говорить, что есть ассоциация между классом Person и Car:

Ассоциация Person Car

class Person {
   private String name;
   private Car car;
   …
}

class Car {
   …
}

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

2.2. Агрегация

Объект класса Keyboard создается извне PC и передается в конструктор для установления связи. Если объект класса PC будет удален, объект класса Keyboard может и дальше использоваться, если, конечно, на него останется ссылка:

Агрегация

public class PC {
    private Keyboard keyboard;

    public PC (Keyboard keyboard) {
        this.keyboard = keyboard;
    }
}

2.3. Композиция

Теперь посмотрим на реализацию композиции. Объект класса Keyboard создается в конструкторе, что означает более тесную связь между объектами. Объект класса Keyboard не может существовать без создавшего его объекта PC:

Композиция

public class PC {
    private Keyboard keyboard;

    public PC () {
        this.keyboard = new Keyboard();
    }
}

3. Композиция против Наследования (Composition over Inheritance)

В объектно-ориентированном проектировании существует важное правило: отдавайте предпочтение композиции перед наследованием.

Почему наследование может быть опасным?

Хотя наследование — мощный инструмент, чрезмерное его использование приводит к проблемам:

  • Хрупкость кода: Если вы измените логику в базовом классе, это может неожиданно «сломать» десятки наследников.

  • Нарушение инкапсуляции: Подкласс часто зависит от деталей реализации родителя.

  • Жесткая иерархия: Вы не можете изменить родителя во время выполнения программы.

  • Проблема «множественного наследования»: В Java нельзя наследоваться от двух классов сразу. Если вам нужны функции и «Принтера», и «Сканера», вы не сможете создать «МФУ» через наследование.

Представим, что мы создаем систему для управления персоналом.

Плохой подход (Наследование):

class Employee {}
class Driver extends Employee {} // А если водитель станет менеджером? Нам придется создавать новый объект.

Хороший подход (Композиция):

Вместо того чтобы говорить, что сотрудник является водителем, мы скажем, что у сотрудника есть роль.

class Role {}
class DriverRole extends Role {}
class ManagerRole extends Role {}

class Employee {
    private Role role; // Композиция: сотрудник "имеет" роль

    public void setRole(Role role) {
        this.role = role; // Мы можем сменить роль в любой момент!
    }
}

Когда что выбирать?

  1. Используйте наследование, только если подкласс является полной и логической заменой родителя (принцип подстановки Лисков). Если вы чувствуете, что наследуете класс только ради того, чтобы «подсмотреть» его методы — используйте композицию.

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

Важное предостережение: Глубокие иерархии наследования (более 3-х уровней) делают код практически нечитаемым и очень сложным в тестировании. Чем меньше уровней в вашей «родословной» классов, тем стабильнее ваша система.

 

Презентацию с видео можно скачать на Patreon.

Read also:
Comments
Droni4
Sep 28, 2022
Ассоциация – это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель. Вполне естественно, что он не будет являться наследником двигателя (хотя такая архитектура тоже возможна в некоторых ситуациях). Выделяют два частных случая ассоциации: композицию и агрегацию.