Многие разработчики при обсуждении основ 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.

EDIT: Другие примеры:

Парадигма ФП:

const formatUser = (user: any) => ({
  name: user.firstName + ' ' + user.lastName,
  age: user.age,
});

const formatAdmin = (admin: any) => ({
  name: admin.fullName,
  permissions: admin.rights,
});

const formatGuest = (guest: any) => ({
  name: guest.nickname,
  temp: true,
});

После понимания и улучшения:

type Formatter<T> = (input: T) => { name: string } & Record<string, any>;

const createNameFormatter = <T>(getName: (x: T) => string, extras: (x: T) => object): Formatter<T> =>
  (input) => ({
    name: getName(input),
    ...extras(input),
  });

const formatUser = createNameFormatter(
  (u) => `${u.firstName} ${u.lastName}`,
  (u) => ({ age: u.age })
);

const formatAdmin = createNameFormatter(
  (a) => a.fullName,
  (a) => ({ permissions: a.rights })
);

const formatGuest = createNameFormatter(
  (g) => g.nickname,
  () => ({ temp: true })
);

Комментарии (14)


  1. dyadyaSerezha
    07.08.2025 11:45

    1) Пример кода со стратегией непонятный (названия особенно) или неполный.

    2) Я бы и с тремя вариантами оставил if-ы, если нет чётких требований/знаний, что в скором времени будут добавляться новые, и, главное, что это единственное место, где надо что-то менять при добавлении нового или изменении старого.

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

    4) А SOLID там никак не упоминается? Устарел или как?

    5) В жизни все так и делают. Для этого и существует такая штука как рефакторинг.


    1. BoburF Автор
      07.08.2025 11:45

      1. Этот пример слишком мал и недостаточно проработан, чтобы его объективно критиковать.

      2. Три варианта можно оставить, но повторения делают код уже трудночитаемым (появился условные параметры).
        Применение паттерна Strategy улучшит читаемость.

      3. Принцип KISS не поощряет дублирование — он говорит о необходимости упрощать код.

      4. В каком контексте вы хотите применить принципы SOLID?

      5. Согласен.


      1. dyadyaSerezha
        07.08.2025 11:45

        но повторения делают код уже трудночитаемым

        Не увидел варианта без повторений.

        Принцип KISS поощряет что угодно, если это делает код проще.

        SOLID как часть чистого кода.


        1. BoburF Автор
          07.08.2025 11:45

          Я не приводил других примеров, потому что считал текущий достаточным. Возможно, добавлю ещё примеры позже.

          RUG — это не про упрощение, а про то, чтобы позволить себе временные компромиссы, пока они не начнут негативно сказываться на коде.

          SOLID — это не «чистый код». Почему?
          Потому что Clean Code — это фундаментальный подход, применимый ко всем парадигмам программирования.
          А SOLID — это специфический набор принципов, актуальный только для ООП.

          Я говорил именно о чистом, понятном коде в целом, а не только об объектно-ориентированном.
          Да, в примере используется ООП, но это не значит, что RUG должен обязательно сочетаться с SOLID.
          RUG — независимый принцип.


          1. dyadyaSerezha
            07.08.2025 11:45

            Согласен со всем, кроме того, что два if-а это компромисс. Это классический KISS и это есть хорошо "в моменте". Более того, я видел кучу кода, где таких if-ов было по 8-10 штук и даже тогда это было оправдано в какой-то степени. Редактировать и проверять в одном месте - уже хорошо.


            1. BoburF Автор
              07.08.2025 11:45

              Ну это всегда зависит от контекста:
              Если там нет такого большого логики то это оправдано
              Но если там будет подключится проверка с запросами в базу или ещё какое-то например алгоритмы то лучше эти вещи отдельно сделать

              Но в целом согласен с вами


        1. BoburF Автор
          07.08.2025 11:45

          Я реально рад что кто то прочитал и критикует. Спасибо вам)


  1. SolidSnack
    07.08.2025 11:45

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

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

    А драить, киссить и солидировать вы будете только на собеседовании, скорее всего)


  1. dimonier
    07.08.2025 11:45

    Спасибо за объяснение и примеры, но в моей картине мира это выглядит по-другому.

    RUG (Repeat Until Good) — это принцип, который говорит: улучшай код итеративно, пока он не станет достаточно хорошим для поставленных целей. Как прогрессивный JPEG.

    То есть, наговнякай, чтобы работало хоть как-то, а потом доводи но нужного уровня хорошести.