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