x
 
Фёдор Борщёв
6 февраля 2020
Советы почтой каждую неделю
Пожалуйста, получите наше письмо, чтобы подтвердить свой адрес:
Вы подписаны на «Советы за неделю»:

Аккуратный код: зацепление


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

  • Зацепление

  • Связность

  • Заменяемость

Слож­ней­ший вопрос в объ­ектно‑ори­ен­ти­ро­ван­ном про­грам­ми­ро­ва­нии — как пра­вильно струк­ту­ри­ро­вать новый код в при­ло­же­нии: поло­жить в отдель­ный класс или про­сто допи­сать в конце суще­ству­ю­щего? Если долго скла­ды­вать код непра­вильно — кодо­вая база дегра­ди­рует. Чтобы пра­вильно опре­де­лять место для нового кода, нужно изу­чить несколько поня­тий — зацеп­ле­ние, связ­ность и заме­ня­е­мость. В этом совете пого­во­рим о зацеплении.

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

  • Зацепление

  • Связность

  • Заменяемость

Зацеп­ле­ние — это мера того, насколько класс в коде зави­сит от сосед­них клас­сов. Чем больше класс зави­сит от сосе­дей, тем выше зацеп­ле­ние. Чем зацеп­ле­ние ниже — тем легче читать и дора­ба­ты­вать код.

Пред­ста­вим бэкенд для мага­зина, кото­рый про­даёт элек­трон­ные курсы. В коде у него есть класс «Заказ», у кото­рого есть метод «отпра­вить поль­зо­ва­телю ссылку на курс». Метод исполь­зует элек­трон­ную почту, но дол­жен ли он рабо­тать с поч­то­вым про­вай­де­ром напря­мую? Давайте попробуем:

from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
​
class Order:
    def ship(self):
        message = Mail(to_emails=self.customer.email, subject='Вот ваш курс', content='Удачи в про­слу­ши­ва­нии')

        sendgrid = SendgridAPIClient(settings.SENDGRID_API_KEY)
        sendgrid.send(message)

Полу­чился код, в кото­ром класс «Заказ» намертво зацеп­лен с клас­сами Mail и SendGridAPIClient из поставки Сенд­грида. Если появятся дру­гие классы, кото­рые пишут письма поль­зо­ва­телю — к при­меру «Дого­ня­тель», кото­рый через три дня после покупки спра­ши­вает у поль­зо­ва­теля, как ему курс, то при­дётся туда копи­ро­вать эту логику:

class Chaser:
    def chase(self):
        message = Mail(to_emails=self.customer.email, subject='Как вам курс?', content='Рас­ска­жите, что нам улуч­шить?')

        sendgrid = SendgridAPIClient(settings.SENDGRID_API_KEY)
        sendgrid.send(message)

Такой оди­на­ко­вый код про­грам­ми­сты назы­вают «копи­пас­той» — по имени опе­ра­ции copy‑paste , кото­рая его порож­дает. Копи­па­ста при­во­дит к боль­шому коли­че­ству ненуж­ной работы. Допу­стим, Сенд­грид стал хуже достав­лять наши письма и, чтобы спа­сти ситу­а­цию, мы решили перейти к дру­гому поч­то­вому про­вай­деру, ска­жем, к Мейл­джету. АПИ Мейл­джета несов­ме­стим с АПИ Сенд­грида, и поэтому нам при­дётся искать в коде все места, кото­рые зацеп­лены с реа­ли­за­цией Сенд­грида и пол­но­стью пере­де­лы­вать их на Мейл­джет — отдельно пра­вить и «Заказ», и «Дого­ня­тель». Если таких мест в коде 5 — при­дётся пра­вить все 5.

Теперь давайте отце­пим «Заказ» и «Дого­ня­тель» от Сенд­грида. Для этого сосре­до­то­чим логику отправки в одном месте — спе­ци­аль­ном классе «Почтальон»:

from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
            ​
class Mailer:
    def __init__(self, customer):
        self.customer = customer
​
    def deliver(self, subject, message):
        message = Mail(to_emails=self.customer.email, subject=subject, content=message)

        sendgrid = SendgridAPIClient(settings.SENDGRID_API_KEY)
        sendgrid.send(message)

Теперь ни «Заказ», ни «Дого­ня­тель» ничего не знают о том, как мы отправ­ляем почту. Их задача — про­сто попро­сить поч­та­льона доста­вить письмо:

from app.mailmain import Mailman
​
class Order:
    def ship(self):
        mailman = Mailman(customer)
        mailman.deliver(subject='Вот ваш курс', content='Удачи в про­слу­ши­ва­нии')

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

P. S. Это был совет об управлении разработкой. Хотите больше знать о планировании спринтов, управлении продуктом или о настройке инфраструктуры? Присылайте вопросы.

Поделиться
Отправить

Комментарии

Серёжа Копылов
7 февраля 2020

На готовом проекте всегда просто сказать: вот смотри, у нас несколько частей используют общий модуль для отправки писем потому, что это правильно и хорошо.

Но в момент разработки даже самые опытные программисты не могут достоверно предсказать, что будет переиспользоваться ещё где-то, а что нет. Например, в примере выше можно заранее вычленить «Почтальона» в отдельный класс, а потом у бизнеса сменятся цели и «Догонятель» не будет написан никогда. А «Почтальон» и 50 других хорошо написанных, но избыточных классов станут тяжёлой ношей, замедляющей разработку, воруя время на практические задачи.

Представляется, что хорошая практика такая: если в системе появляется третье место, где используется то же, что и в предыдущих двух, — выноси это в отдельную сущность.

Saemon Zixel
11 февраля 2020

Логично, что есть тот, кто занимается отправкой писем. Изначально это был sendgrid, потом сделали свой класс. Я тоже сделал бы свой класс, но эмулировал АПИ sendgrid, чтобы потом автозаменой сменить имя класса во всех скриптах.

Но более странным я считаю наличие метода «отправить пользователю ссылку на курс» у класса «Заказ», который не занимается отправкой чего-либо кому-либо. В моём понимании «Заказ» внутри себя хранит: номер, дату, статус, покупатель, выбранный товар, доставку, оплату и другую полезную информацию. И его задача работать с тем, что внутри него, а не слать что-то наружу.

Можно у класса Покупатель сделать метод «отправить письмо» или просто «отправить» и туда передать что-то при вызове.

Кто должен тогда принимать решение и запускать отсылку ссылки на курс? Я думаю, что код присваивающий новый статус заказа, например, на «оплачен» или сервис, проверяющий регулярно, что по всем оплаченным заказам в базе ссылки на курс успешно отправлены заказчикам.


Цель рубрики — обсуждение вопросов дизайна всех видов, текста в дизайне и взаимоотношений дизайнеров с клиентами.

Мы публикуем комментарии, которые добавляют к уже сказанному новые мысли и хорошие примеры. Мы ожидаем, что такие комментарии составят около 20% от общего числа.

Решение о публикации принимается один раз; мы не имеем возможности комментировать или пересматривать свое решение, хотя оно может быть ошибочно. Уже опубликованные комментарии могут быть удалены через некоторое время, если без них обсуждение не становится менее ценным или интересным.

Вот такой веб 2.0.

Почему лучше разделить фронтенд и бэкенд? 7 Что показывает уровень развития инженерной культуры? 1 Разработчик, который не думает, а просто делает — не нужен? 5 Как следить за качеством кода? Часть третья: процессы 4




Недавно всплыло

Нужно ли писать века римскими цифрами? 3 1 1 Советы о дизайне интерфейсов 4