Принцип DRY, вероятно, является первой концепцией разработки программного обеспечения, которую вы усвоите, когда начнете писать код. Звучит очень серьезно и убедительно, ведь аббревиатура есть! Кроме того, идея не повторяться глубоко перекликается с причиной, по которой многие из нас любят программировать компьютеры: чтобы освободить нас от отупляющей повторяющейся работы. Это концепция, которую очень легко понять и объяснить (мне все еще приходится гуглить замену Лискова всякий раз, когда я обсуждаю SOLID-дизайн), и ее применение обычно вызывает тот замечательный шум, который получает ваш мозг, когда он соответствует шаблону. Что не нравится?

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

DRY-код может привести к сильной связи

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

class OrderService:
  # ...
  def send_order_receipt(self, user_id, order_id):
    user = UserService.get(user_id)
    subject = f"Order {order_id} received" 
    body = f"Your order {order_id} has been received and will be processed shortly"
    content = render('user_email.html', user=user, body=body)
    email_provider.send(user.email_address, subject, content)

class PaymentService:
  # ...
  def send_invoice(self, user_id, order_id):
    user = UserService.get(user_id)
    subject = f"Payment for {order_id} received" 
    body = f"Payment for order {order_id} has been received, thank you!"
    content = render('user_email.html', user=user, body=body)
    email_provider.send(user.email_address, subject, content)

Посмотрите на весь этот повторяющийся код! Очень заманчиво СУШИТЬ его:

def send_transaction_email(user_id, order_id, subject, body):
  user = UserService.get(user_id)
  content = render('user_email.html', user=user, body=body)
  email_provider.send(user.email_address, subject, content)

Отлично! Мы извлекли общий код между сервисами во вспомогательную функцию, и теперь наши сервисы выглядят так:

class OrderService:
  # ...
  def send_order_receipt(self, user_id, order_id):
    subject = f"Order {order_id} received" 
    body = f"Your order {order_id} has been received and will be processed shortly"
    send_transaction_email(user_id, ,order_id, subject, body)

class PaymentService:
  # ...
  def send_invoice(self, user_id, order_id):
    subject = f"Payment for {order_id} received" 
    body = f"Payment for order {order_id} has been received, thank you!"
    send_transaction_email(user_id, ,order_id, subject, body)

Намного чище, не правда ли?

Одним из обещаний DRY является то, что он позволит нам лучше развивать наше программное обеспечение; бизнес-требования и технические ограничения меняются все время, и если нам нужно изменить способ поведения этого фрагмента кода, мы изменяем его только один раз, и он будет отражаться повсюду.

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

Но применяемый вслепую, СУХОЙ код может сделать прямо противоположное облегчению изменений. Рассмотрим в нашем примере: что, если из-за бизнес-решения, в почтовом сообщении со счетом PaymentService должен использоваться другой шаблон, как бы мы облегчили это? Или если OrderService теперь требуется для получения списка купленных товаров и внесения его в шаблон электронной почты? Наше извлечение общей логики в метод send_transaction_email привело к тому, что OrderService и PaymentService стали тесно связанными: вы не можете изменить одно без другого.

Как однажды научил меня мой хороший друг Охад Басан:

«Когда вы встречаетесь с классом Helper, последнее, что он сделает, - это поможет вам».

СУХОЙ код может быть труднее читать

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

func TestWebserver_bad_path_500(t *testing.T) {
  srv := createTestWebserver()
  defer srv.Close()

  resp, err := http.Get(srv.URL + "/bad/path")
  if err != nil {
    t.Fatal("failed calling test server")
  }
  if resp.StatusCode != 500 {
    t.Fatalf("expected response code to be 500")
  }

  body, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    t.Fatal("failed reading body bytes")
  }

  if string(body) != "500 internal server error: failed handling /bad/path" {
    t.Fatalf("body does not match expected")
  }
}

func TestWebserver_unknown_path_404(t *testing.T) {
  srv := createTestWebserver()
  defer srv.Close()

  resp, err := http.Get(srv.URL + "/unknown/path")
  if err != nil {
    t.Fatal("failed calling test server")
  }

  if resp.StatusCode != 404 {
    t.Fatalf("expected response code to be 400")
  }

  if resp.Header.Get("X-Sensitive-Header") != "" {
    t.Fatalf("expecting sensitive header not to be sent")
  }
}

Много дублирования для рефакторинга! Оба теста делают примерно одно и то же: они запускают тестовый сервер, делают для него вызов GET, а затем запускают простые утверждения на http.Response.

Мы можем выразить это с помощью вспомогательной функции, которая делает именно это:

func runWebserverTest(t *testing.T, request Requester, validators []Validator) {
  srv := createTestWebserver()
  defer srv.Close()

  response := request(t, srv)
  for _, validator := range validators {
    validator.Validate(t, response)
  }
}

Я редактирую здесь точные определения Requester и Validator для экономии места, но вы можете увидеть полную реализацию в этой сути.

Теперь наши тесты можно сделать красивыми и СУХИМИ:

func Test_DRY_bad_path_500(t *testing.T) {
  runWebserverTest(t,
    getRequester("/bad/path"),
    []Validator{
      getStatusCodeValidator(500),
      getBodyValidator("500 internal server error: failed handling /bad/path"),
    })
}

func Test_DRY_unknown_path_404(t *testing.T) {
  runWebserverTest(t,
    getRequester("/unknown/path"),
    []Validator{
      getStatusCodeValidator(404),
      getHeaderValidator("X-Sensitive-Header", ""),
    })
}

Несколько интересных моментов, которые следует отметить в связи с этим изменением:

  • Будет быстрее писать новые похожие тесты. Если у нас есть 15 разных конечных точек, которые ведут себя одинаково и требуют одинаковых утверждений, мы можем выразить их очень кратко и эффективно.
  • Наш код стало значительно труднее читать и расширять. Если наш тест не удастся из-за каких-либо изменений в будущем, бедному человеку, отлаживающему проблему, придется много щелкать, пока он не поймет, что происходит: мы заменили тривиальный, плоский, прямой код на умные абстракции и косвенные указания.

Но когда мы должны СУШИТЬ наш код?

Встраивание общего кода в библиотеки, которые могут совместно использоваться приложениями, - это проверенная и эффективная практика, хорошо зарекомендовавшая себя в нашей отрасли, и, конечно же, мы не хотим сказать, что должны прекратить это делать!

Чтобы помочь нам решить, когда нам следует СУХОЙ код, я хотел бы представить идею из потрясающей книги, которая недавно была выпущена во втором обновленном издании: Прагматичный программист Энди Ханта и Дэйва Томаса:

«Вещь хорошо спроектирована, если она адаптируется к людям, которые ею пользуются. Для кода это означает, что он должен адаптироваться путем изменения. Поэтому мы верим в принцип ETC: легче менять. И Т.П. Вот и все.

Насколько мы можем судить, каждый принцип дизайна - это особый случай ETC. Почему развязка хорошая? Потому что, изолируя проблемы, мы облегчаем изменение каждой из них. ETC.

Чем полезен принцип единственной ответственности? Потому что изменение требований отражается изменением только одного модуля. ETC.

Томас, Дэвид. Программист-прагматик, 2-е издание, Тема 8: Суть хорошего дизайна

В этой жемчужине главы Хант и Томас исследуют идею о существовании метапринципа для оценки проектных решений, которые часто противоречат друг другу - насколько легко будет развивать нашу кодовую базу, если мы выберем этот конкретный путь? В наших обсуждениях выше мы показали два способа, с помощью которых код DRYing может усложнить его изменение, либо за счет тесной связи, либо за счет затруднения читабельности, что противоречит метапринципу ETC.

Осведомленность об этих возможных последствиях использования СУХОГО кода может помочь решить, когда мы должны не СУХИРОВАТЬ наш код; чтобы узнать, когда мы должны это сделать, давайте вернемся к исходному отрывку из Священных Писаний и еще раз рассмотрим этот принцип.

Принцип DRY был впервые представлен миру теми же Хантом и Томасом в издании книги 2000 года, и поэтому они пишут:

«Каждая часть знания должна иметь единственное, недвусмысленное и авторитетное представление в системе. Альтернативный вариант - выразить одно и то же в двух или более местах.

Если вы измените одно, вы должны не забыть изменить другие, [..]. Вопрос не в том, запомните ли вы: вопрос в том, когда вы забудете.

Томас, Дэвид. Прагматичный программист, 2-е издание. Тема 9 - Зло дублирования

Обратите внимание, что принцип DRY изначально вообще не имеет отношения к повторению или дублированию кода, вместо этого он обсуждает опасность отсутствия единого источника истинного представления для части знания в системе.

Когда мы реорганизовали метод send_transaction_email для замены дублированного кода между OrderService и PaymentService, мы перепутали дублированный код и дублированные знания. Если две процедуры идентичны в определенный момент времени, нет гарантии, что они будут оставаться таковыми в будущем. Мы должны уметь различать процедуры, которые совпадают по совпадению, и те, которые по существу разделяются.

Завершая полный круг, я должен признать, что принцип DRY - это, в конце концов, довольно важный совет; мы должны просто помнить, что, несмотря на то, что его все время бросают:

Заключение

  • Удаление дупликации кажется приятным, но часто ошибочным.
  • Принцип DRY не касается дублирования кода.
  • Мета-принцип хорошего дизайна - ETC.

Первоначально опубликовано на заполнителе 18 мая 2020 г.