Аккуратный код

Аккуратный код

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

Решим задачу «в лоб», расширив класс заказа:

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. Это был совет об управлении разработкой. Хотите больше знать о планировании спринтов, управлении продуктом или о настройке инфраструктуры? Присылайте вопросы.
Управление проектомВеб‑разработка
Отправить
Поделиться
Запинить

Рекомендуем другие советы