Аккуратный код
Заменяемость
Аккуратный код
Заменяемость
Давайте представим, что наше приложение для продажи курсов начинает работать не только с записями лекций, но и с билетами на онлайн‑события, скажем, на вебинары. С лекциями всё было просто: мы скидывали всем купившим ссылку на скачивание и считали заказ выполненным. С вебинарами сложнее — придётся регистрировать пользователя на площадке и присылать ему приватную ссылку на комнату.
Решим задачу «в лоб», расширив класс заказа:
from app.mailman import Mailman
from app.integrations import Zoom
class ZoomOrder(Order):
def ship(self, zoom_webinar_id):
self.register_in_zoom_room(zoom_course_id)
def register_in_zoom_room(self, zoom_webibar_id):
zoom_client = new Zoom(customer=self.customer, webinar_id=zoom_webinar_id)
link = zoom_client.register()
self.save_link_to_db(link)
mailman = Mailman(customer)
mailman.deliver(subject='ссылка на вебинар', content=f'Вот: {link}')
Новому классу нужно знать, на какой вебинар регистрировать пользователя, и мы не придумали для этого ничего лучше, чем добавить обязательный параметр в метод ship(). Получается, что ZoomOrder не совместим с обычным Order, а значит, во всех местах, которые формируют заказ, придётся делать примерно так:
order = Order(customer)
if type == 'zoom':
order.ship(zoom_webinar_id=request.data['ZoomWebinarId'])
else:
order.ship()
Такой код придётся искать и переписывать всякий раз, когда у нас будет добавляться новый вид товаров. Это и есть нарушение принципа заменяемости:
Давайте сделаем новый сфокусированный базовый класс «Товар» с двумя специфичными наследниками — «Лекцией» и «Вебинаром». Пусть каждый наследник несёт в себе всю бизнес‑логику, которая включается, когда его купили:
class Item:
"""Абстрактный товар"""
def __init__(self, request):
self.request = request
def ship(self):
"""Что случается после продажи"""
pass
class Lecture(Item):
def ship(self):
mailman = Mailman(self.request.customer)
mailman.deliver(subject='Вот ваша лекция', content='Удачи в прослушивании')
class ZoomWebinar(Item):
def ship(self):
zoom_client = new Zoom(customer=self.request.customer, webinar_id=self.request.data['zoom_webinar_id'])
...
mailman.deliver(subject='ссылка на вебинар', content=f'Вот: {link}')
Оба класса реализуют стандартный интерфейс, поэтому код для записей и вебинаров выглядит одинаково:
item = Lecture(request) # отправить запись лекции
item.ship()
item = ZoomWebinar(request) # зарегистрировать на вебинар
item.ship()
Когда каждый товар сам знает, что делать после продажи, класс заказа становится более сфокусированным. Его задача теперь — определить, что положили в корзину, и оформить продажу:
class Order:
def __init__(self, request):
self.request = request
@property
def item(self):
if 'zoom_webinar_id' in self.request:
return ZoomWebinar(request)
return Lecture(request)
def ship(self):
self.item.ship()
У нас получилась устойчивая к изменениям система, в которую легко можно добавить любую другую вебинарную площадку или даже целый класс товаров, к примеру доставку книг или подписку на платную рассылку.
P. S. Это был совет об управлении разработкой. Хотите больше знать о планировании спринтов, управлении продуктом или о настройке инфраструктуры? Присылайте вопросы.