1. Введение
Объекты имеют отношения между собой, как в реальной жизни, так и в программировании. Иногда трудно понять или реализовать эти отношения.
В этом уроке мы сосредоточимся на подходе Java к трем типам отношений, которые иногда легко перепутать: композиция, агрегация и ассоциация.
2. Состав
Композиция
— это тип отношений «принадлежит». Это означает, что один из объектов является логически большей структурой, содержащей другой объект. Другими словами, это часть или член другого объекта.
В качестве альтернативы мы часто называем это отношением «имеет-а» (в отличие от отношения «есть-а», которое является наследованием ).
Например, комната принадлежит зданию, или, другими словами, в здании есть комната. Таким образом, в основном, называем ли мы это «принадлежит» или «имеет», это только вопрос точки зрения.
Композиция — это сильный вид отношения «имеет», потому что содержащий объект владеет ею. Следовательно, жизненные циклы объектов связаны. Это означает, что если мы уничтожим объект-владелец, его члены также будут уничтожены вместе с ним. Например, комната уничтожена вместе со зданием в нашем предыдущем примере.
Обратите внимание, это не означает, что содержащий объект не может существовать без каких-либо его частей. Например, мы можем снести все стены внутри здания, а значит, разрушить комнаты. Но здание все равно будет существовать.
С точки зрения кардинальности, содержащий объект может иметь столько частей, сколько мы хотим. Однако все части должны иметь ровно один контейнер .
2.1. UML
В UML мы обозначаем композицию следующим символом:
Обратите внимание, что ромб находится на содержащем объекте и является основанием линии, а не стрелкой. Для ясности мы часто рисуем и наконечник стрелки:
Итак, мы можем использовать эту конструкцию UML для нашего примера Building-Room:
2.2. Исходный код
В Java мы можем смоделировать это с помощью нестатического внутреннего класса:
class Building {
class Room {}
}
В качестве альтернативы мы также можем объявить этот класс в теле метода. Неважно, именованный это класс, анонимный класс или лямбда:
class Building {
Room createAnonymousRoom() {
return new Room() {
@Override
void doInRoom() {}
};
}
Room createInlineRoom() {
class InlineRoom implements Room {
@Override
void doInRoom() {}
}
return new InlineRoom();
}
Room createLambdaRoom() {
return () -> {};
}
interface Room {
void doInRoom();
}
}
Обратите внимание, что наш внутренний класс должен быть нестатическим, поскольку он привязывает все свои экземпляры к содержащему классу.
Обычно содержащий объект хочет получить доступ к своим членам. Поэтому мы должны хранить их ссылки:
class Building {
List<Room> rooms;
class Room {}
}
Обратите внимание, что все объекты внутреннего класса хранят неявную ссылку на содержащий их объект. В результате нам не нужно хранить его вручную, чтобы получить к нему доступ:
class Building {
String address;
class Room {
String getBuildingAddress() {
return Building.this.address;
}
}
}
3. Агрегация
Агрегация также является отношением «имеет-а». Что отличает его от композиции, так это то, что он не предполагает владения. В результате жизненные циклы объектов не связаны: каждый из них может существовать независимо друг от друга.
Например, автомобиль и его колеса. Мы можем снять колеса, и они все еще будут существовать. Мы можем установить другие (существовавшие ранее) колеса или установить их на другой автомобиль, и все будет работать нормально.
Конечно, автомобиль без колес или с оторванным колесом не будет так полезен, как автомобиль с колесами. Но именно поэтому изначально существовали эти отношения: чтобы собрать части в более крупную конструкцию, которая способна на большее, чем ее части .
Поскольку агрегация не предполагает владения, члену не нужно привязываться только к одному контейнеру . Например, треугольник состоит из сегментов. Но треугольники могут иметь общие сегменты в качестве сторон.
3.1. UML
Агрегация очень похожа на композицию. Единственное логическое отличие состоит в том, что агрегация является более слабой связью.
Поэтому представления UML также очень похожи. Единственная разница в том, что ромб пуст:
Тогда для автомобилей и колес мы бы сделали:
3.2. Исходный код
В Java мы можем смоделировать агрегацию с помощью старой простой ссылки:
class Wheel {}
class Car {
List<Wheel> wheels;
}
Член может быть классом любого типа, кроме нестатического внутреннего класса.
В приведенном выше фрагменте кода оба класса имеют отдельный исходный файл. Однако мы также можем использовать статический внутренний класс:
class Car {
List<Wheel> wheels;
static class Wheel {}
}
Обратите внимание, что Java создаст неявную ссылку только в нестатических внутренних классах. Из-за этого мы должны поддерживать отношения вручную там, где нам это нужно:
class Wheel {
Car car;
}
class Car {
List<Wheel> wheels;
}
4. Ассоциация
Ассоциация - самая слабая связь между тремя. Это не отношение «имеет» , ни один из объектов не является частью или членом другого.
Ассоциация означает только то, что объекты «знают» друг друга. Например, мать и ее ребенок.
4.1. UML
В UML мы можем пометить ассоциацию стрелкой:
Если ассоциация двунаправленная, мы можем использовать две стрелки, стрелку со стрелкой на обоих концах или линию без стрелок:
Мы можем представить мать и ее ребенка в UML, тогда:
4.2. Исходный код
В Java мы можем моделировать ассоциацию так же, как агрегацию:
class Child {}
class Mother {
List<Child> children;
}
Но подождите, как мы можем определить, означает ли ссылка агрегацию или ассоциацию?
Ну, мы не можем. Разница только логическая: является ли один из объектов частью другого или нет.
Кроме того, мы должны поддерживать ссылки вручную на обоих концах, как мы это делали с агрегацией:
class Child {
Mother mother;
}
class Mother {
List<Child> children;
}
5. Дополнительная информация об UML
Ради ясности иногда мы хотим определить кардинальность отношения на диаграмме UML. Мы можем сделать это, написав его на концах стрелки:
Обратите внимание, что нет смысла записывать ноль как мощность, потому что это означает отсутствие связи. Единственным исключением является случай, когда мы хотим использовать диапазон для указания необязательного отношения:
Также обратите внимание, что поскольку в составе ровно один владелец, то на схемах его не указываем.
6. Сложный пример
Давайте посмотрим (немного) более сложный пример!
Мы смоделируем университет, в котором есть свои факультеты. На каждой кафедре работают профессора, у которых также есть друзья между собой.
Будут ли кафедры существовать после того, как мы закроем университет? Конечно нет, поэтому это композиция.
Но профессора останутся (надеюсь). Мы должны решить, что логичнее: считать профессоров частью кафедр или нет. Альтернативно: являются они членами отделов или нет? Да. Следовательно, это совокупность. Кроме того, профессор может работать в нескольких отделах.
Отношения между профессорами являются ассоциативными, потому что нет никакого смысла говорить, что профессор является частью другого.
В результате мы можем смоделировать этот пример с помощью следующей диаграммы UML:
И код Java выглядит так:
class University {
List<Department> department;
}
class Department {
List<Professor> professors;
}
class Professor {
List<Department> department;
List<Professor> friends;
}
Обратите внимание, что если мы полагаемся на термины «имеет», «принадлежит», «член», «часть» и т. д., мы можем легче определить отношения между нашими объектами.
7. Заключение
В этой статье мы увидели свойства и представление композиции, агрегации и ассоциации. Мы также увидели, как моделировать эти отношения в UML и Java.
Как обычно, примеры доступны на GitHub .