Композиция

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

Чтобы понять, зачем нужна композиция в программировании, проведем аналогию с реальным миром. Большинство биологических и технических объектов состоят из более простых частей, также являющихся объектами. Например, животное состоит из различный органов (сердце, желудок), компьютер — из различного "железа" (процессор, память).

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

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

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

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

По условию задачи обои клеятся только на стены, следовательно площади верхнего и нижнего прямоугольников нам не нужны. Из рисунка видно, что площадь одной стены равна xz, второй – уz. Противоположные прямоугольники равны, значит общая площадь четырех прямоугольников равна S = 2xz + 2уz = 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, lenght, height и wd. Площади (полная и оклеиваемая) должны вычислять лишь при необходимости путем вызова методов.

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

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

Курс с примерами решений практических работ: android-приложение, pdf-версия.

Создано

Обновлено

Комментарии

При запуске кода возникает следующая ошибка: E0202:An attribute defined in test line 15 hides this method (14, 5)

13...
14 def wallpapers(self):
15        self.wallpapers = self.square - self.window.square * self.numb_w - self.door.square * self.numb_d
16...

Вот, нашел описание данной ошибки, но не могу понять ее суть и соответственно сообразить что не так с кодом.

Ошибка E0202

Описание
Используется, когда класс определяет метод, который скрыт атрибутом экземпляра с тем же именем.

Это сообщение принадлежит проверке классов.

Примерами являются:

класс определяет метод, и суперкласс задает атрибут экземпляра с тем же именем.
класс определяет метод, а клиент устанавливает атрибут экземпляра с тем же именем.

Помогите разобраться с данным вопросом. Заранее благодарен.

Ответ на от Дмитрий

Похоже на то, что нельзя определять атрибут и метод с одним и тем же именем. Случай: 

класс определяет метод, а клиент устанавливает атрибут экземпляра с тем же именем

В данном случае есть метод wallpapers() и атрибут wallpapers. Первый вызывается например так labor34.wallpapers(), второй так labor34.wallpapers (внутри метода как self.wallpapers). У меня программа интерпретируется без ошибки. Возможно зависит от среды разработки. 

И все таки, что-то не так с win_door/Win_Door. Выдает ошибку при любом регистре букв.

Traceback (most recent call last):
  File "part.py", line 17, in <module>
    kitchen.win_door(1.8, 2, 2, 2)
  File "part.py", line 5, in win_door
    self.window = win_door(c, d)
NameError: name 'win_door' is not defined