Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров1.2K

Многие разработчики при обсуждении основ 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 })
);
Теги:
Хабы:
+1
Комментарии10

Публикации

Ближайшие события