Принцип DRY: палка о двух концах

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

Опасности чрезмерной инженерии

Представьте, что вы работаете над простой задачей, например, рассчитываете цену товаров со скидками и без них. Без принципа DRY у вас могут быть две отдельные функции:

function calculatePriceWithPercentageDiscount(price: number, discountPercentage: number): number {
    return price * (1 - discountPercentage / 100);
}

function calculatePriceWithFixedDiscount(price: number, discountAmount: number): number {
    return price - discountAmount;
}

Эти функции просты и понятны. Однако во имя соблюдения принципа DRY вы можете объединить их в одну функцию:

function calculatePrice(price: number, discount: number | { type: 'percentage' | 'fixed', value: number }): number {
    if (typeof discount === 'number') {
        return price - discount;
    } else if (discount.type === 'percentage') {
        return price * (1 - discount.value / 100);
    } else {
        throw new Error('Некорректный тип скидки');
    }
}

На первый взгляд это кажется хорошей идеей, но вносит ненужную сложность. Объединённая функция сложнее для понимания и правильного использования, особенно для разработчиков, которые не знакомы с нюансами параметра discount[1].

Удобочитаемость и простота

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

Например, представьте себе сценарий, в котором вам нужно рассчитать возраст пользователей на основе года их рождения. Без применения принципа DRY у вас могли бы быть отдельные функции для разных ролей пользователей:

function calculateStudentAge(birthYear: number): number {
    return new Date().getFullYear() - birthYear;
}

function calculateTeacherAge(birthYear: number): number {
    return new Date().getFullYear() - birthYear;
}

Хотя эти функции идентичны, они понятны и просты для понимания в своих контекстах. Объединение их в одну функцию с дополнительными параметрами или условной логикой только добавит сложности без какой-либо реальной пользы[1].

Преждевременная абстракция

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

Вот пример того, как преждевременная абстракция может привести к ненужной сложности:

classDiagram class PizzaOrderer { + orderPizza() } class HawaiianPizzaOrderer { + orderPizza() } class PepperoniPizzaOrderer { + orderPizza() } PizzaOrderer <|-- HawaiianPizzaOrderer PizzaOrderer <|-- PepperoniPizzaOrderer

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

Связанность и гибкость

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

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

Цена неправильных абстракций

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

// Неправильная абстракция
class ProductController {
    private discountType: 'percentage' | 'fixed';

    constructor(discountType: 'percentage' | 'fixed') {
        this.discountType = discountType;
    }

    calculatePrice(price: number, discount: number): number {
        if (this.discountType === 'percentage') {
            return price * (1 - discount / 100);
        } else {
            return price - discount;
        }
    }
}

// Использование
const percentageController = new ProductController('percentage');
const fixedController = new ProductController('fixed');

console.log(percentageController.calculatePrice(100, 20)); // 80
console.log(fixedController.calculatePrice(100, 20)); // 80

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

Практические советы

Итак, как сбалансировать преимущества принципа DRY с необходимостью обеспечения простоты, удобочитаемости и гибкости?

  1. Сначала пишите код для конкретного случая: перед обобщением убедитесь, что код работает для конкретного варианта использования. Обобщайте и проводите рефакторинг только тогда, когда возникнет необходимость и станут очевидны преимущества[3].

  2. Избегайте преждевременной абстракции: не создавайте абстракции, пока в них нет явной необходимости. Преждевременная абстракция может привести к излишней сложности и связанности[3].

  3. Учитывайте контекст: принцип DRY заключается в том, чтобы избегать дублирования знаний, а не просто любого кода. Убедитесь, что повторяемый вами код действительно представляет собой знания, которые не следует повторять[3].

  4. Отдавайте предпочтение удобочитаемости перед возможностью повторного использования: иногда удобочитаемость и простота важнее возможности повторного использования. Если повторение кода облегчает его понимание и поддержку, это может быть лучшим выбором[1].

  5. Проводите рефакторинг обдуманно: проводите рефакторинг кода только при необходимости и когда преимущества перевешивают затраты. Избегайте рефлекторных реакций на дублирование кода[5].

Заключение

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

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