RUG — малоизвестный, но фундаментальный принцип Clean Code

в 11:15, , рубрики: cleancode, patterns, programming, refactoring, rug, software design, TypeScript

Многие разработчики при обсуждении основ Clean Code называют одни и те же принципы — чаще всего упоминаются DRY, KISS и YAGNI. Эти концепции прочно закрепились в профессиональном сообществе и воспринимаются как обязательная часть хорошего кода.

Принцип RUG упоминается значительно реже. Чаще всего о нём узнают с опытом, а многие применяют его интуитивно, даже не подозревая, что для этого подхода существует отдельное название и формулировка.

Сегодня я хочу поговорить о принципе RUG и о том, какие рекомендации он даёт по написанию программного обеспечения.

RUG (Repeat Until Good) — это принцип, который говорит: можно повторять код, пока это разумно.

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

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

Я буду использовать TypeScript, так как этот язык знаком большинству разработчиков. 😁

enum PaymentProvider {
  AlphaPay = 'ALPHA_PAY',
  BetaPay = 'BETA_PAY',
}

interface CreateCashboxParams {
  provider: PaymentProvider;
  merchantId: string;
}

class CashboxService {
  createCashbox(params: CreateCashboxParams) {
    if (params.provider === PaymentProvider.AlphaPay) {
      // Логика для AlphaPay
      return {
        id: `alpha-${params.merchantId}`,
        provider: params.provider,
        settings: {
          apiKey: 'ALPHA_KEY',
        },
      };
    }

    if (params.provider === PaymentProvider.BetaPay) {
      // Логика для BetaPay
      return {
        id: `beta-${params.merchantId}`,
        provider: params.provider,
        settings: {
          apiKey: 'BETA_KEY',
        },
      };
    }

    throw new Error('Unknown payment provider');
  }
}

Теперь у нас появился новый провайдер с дополнительными настройками:

enum PaymentProvider {
  AlphaPay = 'ALPHA_PAY',
  BetaPay = 'BETA_PAY',
  GammaPay = 'GAMMA_PAY',
}

interface CreateCashboxParams {
  provider: PaymentProvider;
  merchantId: string;
}

class CashboxService {
  createCashbox(params: CreateCashboxParams) {
    if (params.provider === PaymentProvider.AlphaPay) {
      return {
        id: `alpha-${params.merchantId}`,
        provider: params.provider,
        settings: {
          apiKey: 'ALPHA_KEY',
        },
      };
    }

    if (params.provider === PaymentProvider.BetaPay) {
      return {
        id: `beta-${params.merchantId}`,
        provider: params.provider,
        settings: {
          apiKey: 'BETA_KEY',
        },
      };
    }

    if (params.provider === PaymentProvider.GammaPay) {
      return {
        id: `gamma-${params.merchantId}`,
        provider: params.provider,
        settings: {
          apiKey: 'GAMMA_KEY',
          region: 'EU', // дополнительная настройка
        },
      };
    }

    throw new Error('Unknown payment provider');
  }
}

Теперь мы видим, что набор параметров может меняться в зависимости от провайдера, и при создании кассы иногда требуется выполнять разную логику. Это стало очевидно, поэтому настал момент вынести эту часть в отдельные стратегии, применив паттерн Strategy для создания понятной и расширяемой абстракции.

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

interface CreateCashboxParams {
  merchantId: string;
}

class CashboxService {
  createCashbox(params: CreateCashboxParams) {
    return {
      id: `alpha-${params.merchantId}`,
      provider: 'ALPHA_PAY',
      settings: {
        apiKey: 'ALPHA_KEY',
      },
    };
  }
}

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

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

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

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

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

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

Автор: BoburF

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js