Нашли ошибку?
Скачать чистый
урок 3 из 4≈ 20 минупражнения: 0 из 3

Композиция против наследования

«Является» — наследование, «содержит» — композиция. Объект хранит другие объекты и делегирует им работу; почему class Menu(list) — плохая идея и как заменить части объекта на ходу.

Python запустится при первом выполнении

Наследование — не единственный способ построить класс из готовых частей. Корзина из модуля 2 не была подклассом списка или товара — она содержала список товаров. Это второй способ, композиция (composition): объект хранит другие объекты в атрибутах и пользуется ими. Как выбрать между ними?

«Является» или «содержит»

Простой тест — сказать фразу вслух:

  • латте является напитком, сезонный товар является товаром → наследование: class Latte(Drink);
  • заказ содержит корзину, кофемашина содержит бойлер → композиция: self.cart = cart.

Если фраза «X является Y» звучит странно (заказ — это корзина? касса — это список?), наследование не подходит.

Вопрос

Как связать классы Order (заказ) и Customer (покупатель)?

Делегирование

Объект, который содержит другой объект, часто передаёт ему работу. Кофемашина не греет воду сама — она просит об этом свой бойлер. Это называется делегирование (delegation). Кофемашина при этом сама решает, какие возможности бойлера показать снаружи:

[ ]Кофемашина делегирует бойлеру
class Boiler:
    def __init__(self):
        self.temperature = 20

    def heat(self, target):
        self.temperature = target
        return f"вода нагрета до {target} °C"


class CoffeeMachine:
    def __init__(self):
        self.boiler = Boiler()  # кофемашина содержит бойлер

    def brew(self, drink):
        step = self.boiler.heat(93)  # делегирует нагрев бойлеру
        return f"{step}; {drink} готов"


machine = CoffeeMachine()
print(machine.brew("американо"))
print(machine.boiler.temperature)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
вода нагрета до 93 °C; американо готов
93

Для упражнений понадобятся знакомые товар и корзина:

[ ]Product и Cart
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product({self.name!r}, {self.price})"


class Cart:
    def __init__(self):
        self.items = []

    def add(self, product):
        self.items.append(product)

    @property
    def subtotal(self):
        return sum(p.price for p in self.items)


cart = Cart()
cart.add(Product("Латте", 220))
print(cart.items, cart.subtotal)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
[Product('Латте', 220)] 220
[ ]Упражнение 1Заказ с корзиной решено

Объявите класс Order: Order("Анна", cart) хранит имя покупателя в customer_name (для простоты — строкой) и корзину в cart. Метод add(product) кладёт товар в корзину заказа, а свойство total возвращает сумму корзины (subtotal). Не копируйте код корзины — делегируйте ей.

class Order:
    def __init__(self, customer_name, cart):
        ...

    def add(self, product):
        ...

    @property
    def total(self):
        ...

Ловушка: class Menu(list)

Меню — это список товаров, так что хочется написать class Menu(list). Но подкласс получает все методы списка — и вместе с ними способы сломать меню:

[ ]Меню-список принимает что угодно
class ListMenu(list):  # ошибка: меню — не список
    pass


menu = ListMenu()
menu.append(Product("Латте", 220))
menu.append("кирпич")  # и это тоже можно
print(len(menu), type(menu + []).__name__)
menu.clear()
print(menu)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
2 list
[]
[ ]Упражнение 2Меню через композицию решено

Объявите класс Menu без наследования от list. Товары хранятся во внутреннем списке _items. Метод add(product) добавляет товар, а если это не экземпляр Product — выбрасывает TypeError. Метод names() возвращает список названий, метод find(name) — товар с таким названием или None.

class Menu:
    def __init__(self):
        ...

    def add(self, product):
        ...

    def names(self):
        ...

    def find(self, name):
        ...

Части можно менять на ходу

У композиции есть ещё одно преимущество: часть объекта можно заменить другой, пока программа работает. Класс через наследование выбирается раз и навсегда, а атрибут можно переприсвоить. Покупатель передумал и вместо курьера выбрал самовывоз — меняется только атрибут заказа. Способы доставки — классы из модуля 5:

[ ]Самовывоз и курьер
class Pickup:
    def cost(self, km):
        return 0


class Courier:
    def cost(self, km):
        return 150 + 30 * km


print(Pickup().cost(5), Courier().cost(5))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
0 300
[ ]Упражнение 3Заказ с доставкой решено

Объявите класс DeliveryOrder: DeliveryOrder(cart, delivery) хранит корзину в cart и способ доставки в delivery — любой объект с методом cost(km) (например, Courier() или Pickup() из демонстрации). Метод total(km) возвращает сумму корзины плюс стоимость доставки. Затем создайте order — заказ с корзиной cart из заготовки и курьером, сохраните в by_courier его total(5), поменяйте у этого же заказа доставку на самовывоз и сохраните total(5) в by_pickup.

class DeliveryOrder:
    def __init__(self, cart, delivery):
        ...

    def total(self, km):
        ...


cart = Cart()
cart.add(Product("Латте", 220))
order = ...
by_courier = ...
by_pickup = ...

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

Нашли ошибку?