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