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

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/934986/