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

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

Композиция часто остаётся в тени, не выделяясь как основное свойство объектно-ориентированного программирования, наряду с наследованием, инкапсуляцией и полиморфизмом, так как используется реже.

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

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

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

Комната в виде прямоугольного параллелепипеда для расчета площади стен

Поскольку обои клеятся только на стены, то площадь верхнего и нижнего прямоугольников можно не учитывать. Из рисунка видно, что площадь одной стены равна xz, а другой - yz. Поскольку противоположные стороны равны, общая площадь четырёх стен равна S = 2xz + 2yz = 2z(x+y). Затем от общей площади нужно вычесть площадь дверей и окон, поскольку они не оклеиваются.

Мы можем выделить три типа объектов: окна, двери и комнаты. Все они имеют свои классы. Окна и двери являются частями комнаты, так что они включаются в состав объекта «помещение».

Для этой задачи существенны лишь два параметра: длина и ширина. Поэтому классы для «окон» и «дверей» можно объединить в один. Если бы интерес представляли другие свойства, такие как толщина стекла или материал двери, то стоило бы создавать отдельные классы. В данном случае сосредоточимся на вычислении площади объекта:

class Win_Door:
     def __init__(self, x, y):
          self.square = x * y 

Класс «комната» выполняет роль контейнера для окон и дверей. Он обязан вызывать класс «окно-дверь».

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

class Room:
    def __init__(self, x, y, z):
        self.square = 2 * z * (x + y)
        self.wd = []
    def addWD(self, w, h):
        self.wd.append(WinDoor(w, h))
    def workSurface(self):
        new_square = self.square
        for i in self.wd:
            new_square -= i.square
        return new_square

r1 = Room(6, 3, 2.7)
print(r1.square) # выведет 48.6
r1.addWD(1, 1)
r1.addWD(1, 1)
r1.addWD(1, 2)
print(r1.workSurface()) # выведет 44.6

Практическая работа

Описанная выше программа нуждается в улучшениях. Следует провести доработку и исправления, согласно предложенному плану.

При вычислении оклеиваемой площади значение поля self.square остаётся неизменным. Оно всё ещё хранит полную площадь стен и может быть полезно, если изменится список объектов wd, потребующись повторный расчёт оклеиваемой площади.

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

Внесите изменения в код, чтобы объекты класса Room имели четыре поля: width, length, height и wd. Площадь помещений будет вычисляться при обращении к соответствующим методам.

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

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

Вопросы для самопроверки:

  1. Какое преимущество предоставляет композиционный подход в объектно-ориентированном программировании?
  2. В чем различие между композицией и наследованием в объектно-ориентированном программировании?