Site icon AppTractor

Почему DRY — самый переоцененный принцип программирования

Я начну свой новый блог с самого большого кликбейта, который только могу придумать. Я подозреваю, что любой разработчик, читающий это, знает о принципе DRY, потому что он вездесущ. Если нет, вам просто нужно знать, что это означает Don’t Repeat Yourself (“Не повторяйтесь») и обычно к нему обращаются, когда людям советуют не копировать и вставлять фрагменты кода повсюду, а вместо этого объединять логику в одном месте.

DRY был первым принципом программирования, с которым я столкнулся, и, вероятно, единственным, о котором я узнал за первый год работы разработчиком. Это также, вероятно, один из самых простых принципов для понимания. Если вы видите в своем коде две одинаковые вещи, возможно, они должны быть просто одной вещью. Трудно с этим поспорить. Но я думаю, что DRY такой же, как и любой другой принцип — он имеет свое применение, но лучше всего применять его в меру. И я думаю, что из-за его вездесущности и простоты мы склонны с ним заходить слишком далеко и слишком часто.

Итак, без лишних слов, давайте углубимся в три моих критических замечания по поводу DRY.

1. DRY неправильно используется для устранения случайного повторения

Иногда вещи совпадают, но это просто совпадение. Например, рассмотрим некоторый код Python, который запрашивает пиццу из вымышленного API.

def make_hawaiian_pizza():
    payload = {
        crust: "thin",
        sauce: "tomato",
        cheese: "regular",
        toppings: ["ham", "pineapple"]
    }
    requests.post(PIZZA_URL, payload)

def make_pepperoni_pizza():
    payload = {
        crust: "thin",
        sauce: "tomato",
        cheese: "regular",
        toppings: ["pepperoni"]
    }
    requests.post(PIZZA_URL, payload)

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

def make_pizza(toppings):
    payload = {
        crust: "thin",
        sauce: "tomato",
        cheese: "regular",
        toppings: toppings
    }
     requests.post(PIZZA_URL, payload)

def make_pepperoni_pizza():
    make_pizza(["pepperoni"])

def make_hawaiian_pizza():
    make_pizza(["ham", "pineapple"])

 

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

2. DRY создает презумпцию повторного использования

Представьте, что мы в компании с большой кодовой базой, где несколько продуктовых областей хотят интегрировать заказ пиццы. Вместо того, чтобы каждый продукт писал свою собственную функцию make_pizza(), почему бы не поместить ее в common  библиотеку, которую любой продукт может импортировать и вызывать?

Итак, мы идем по этому пути и получаем 5 продуктов, каждый из которых вызывает make_pizza() с разными массивами аргументов для различных типов пиццы, которые они хотят.

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

# cool_product/pizza.py
left_toppings = ["beef"]
right_toppings = [] 
make_pizza(left_toppings, right_toppings)  # this will be a very funny pizza 

# common/make_pizza.py
def make_pizza(*args):
    payload = {
        crust: "original",
        sauce: "tomato",
        cheese: "regular",
    }
    if len(args) == 2:
        payload["toppings_left"] = args[0]
        payload["toppings_right"] = args[1]
    else:
        payload["toppings_left"] = args[0]
        payload["toppings_right"] = args[0]

    return requests.post(PIZZA_URL, payload)

Это работает и не требует изменения каждого существующего использования API. Однако, надеюсь, вы согласитесь, что это не очень хорошо. Значение первого аргумента изменилось из-за того, что вы передали необязательный второй аргумент, что очень странно. Есть много других способов сделать этот рефакторинг, но я утверждаю, что любое изменение, которое не изменяет существующие вызовы make_pizza или не делает полностью отдельную функцию для пиццы с раздельным топпингом (не DRY), будет в некоторой степени плохим.

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

3. DRY — это лекарство, которое ведет к ненужной сложности

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

Чтобы решить мою проблему с соусом, возможно, я мог бы использовать ООП и иметь класс PizzaOrderer, который может быть подклассом для каждого типа пиццы, позволяя каждому типу переопределять разумные значения по умолчанию для соуса/основы. Или, может быть, я мог бы использовать класс для представления пиццы и иметь такие методы, как add_topping()/add_topping_left()/add_topping_right(), чтобы потребители могли быстро добавлять начинки, если они готовят целую пиццу, а также выбирать детализацию для разделенных пицц. Есть много других трюков, которые вы могли бы предложить.

Все эти идеи великолепны. Но помните, что основная цель здесь — отправить POST-запрос с одним объектом JSON. Это очень, очень просто сделать. Теперь мы говорим о всевозможных причудливых программах, чтобы попытаться решить проблемы, которые существуют только потому, что мы не хотим повторять один и тот же 6-строчный фрагмент в нескольких разных местах, потому что DRY говорит нам, что это плохо.

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

И что?

Ну, очевидно, я не говорю, что мы должны полностью выбросить DRY. Я не уверен, что действительно возможно писать код, который «никогда не повторяется». Но я думаю, что мы должны смягчить рефлекторную реакцию на пул-реквесты, которые содержат несколько повторений кода. Есть по крайней мере несколько случаев, когда это может быть правильным решением.

Надеюсь, вам понравился пост, и, пожалуйста, напишите мне в Твиттер, чтобы обсудить!

Источник

Exit mobile version