DRY, KISS e YAGNI
Baixar PDFTrês lembretes que evitam os maiores crimes contra a legibilidade — Don't Repeat Yourself, Keep It Simple, You Aren't Gonna Need It.
DRY, KISS e YAGNI
São três princípios simples que servem como lembretes diários. Não são regras absolutas — são balanças. Quando em dúvida, eles te puxam para o lado da simplicidade.
DRY — Don't Repeat Yourself
"Cada pedaço de conhecimento deve ter uma representação única, autoritária e não ambígua no sistema." — The Pragmatic Programmer (1999)
O que é
Se você copiou e colou código mais de uma vez, provavelmente deveria extrair uma função, componente ou utilitário. Duplicação significa que uma mudança precisa ser feita em vários lugares — e esquecer um deles gera bugs.
Frontend — Componentes duplicados
// ❌ DRY violado — mesmo JSX copiado em 3 páginas
// pages/products.tsx
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{formatCurrency(product.price)}</p>
<button onClick={() => addToCart(product)}>Comprar</button>
</div>
// pages/favorites.tsx
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{formatCurrency(product.price)}</p>
<button onClick={() => addToCart(product)}>Comprar</button>
</div>
// pages/search.tsx — MESMO CÓDIGO de novo
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{formatCurrency(product.price)}</p>
<button onClick={() => addToCart(product)}>Comprar</button>
</div>// ✅ Componente extraído — uma única fonte de verdade
function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{formatCurrency(product.price)}</p>
<button onClick={() => onAddToCart(product)}>Comprar</button>
</div>
);
}
// pages/products.tsx — usa o componente
<ProductCard product={product} onAddToCart={addToCart} />Backend — Validação duplicada
// ❌ DRY violado — mesma validação em 3 rotas
// routes/users.ts
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Email inválido' });
}
// routes/orders.ts
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Email inválido' });
}
// routes/subscriptions.ts
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Email inválido' });
}// ✅ Validação extraída — uma única fonte de verdade
// validators/email.validator.ts
function validateEmail(email: unknown): Result<string, ValidationError> {
if (typeof email !== 'string' || !email.includes('@')) {
return { ok: false, error: new ValidationError('Email inválido', 'email') };
}
return { ok: true, value: email };
}
// routes/users.ts
const emailResult = validateEmail(req.body.email);
if (!emailResult.ok) return res.status(400).json({ error: emailResult.error.message });Quando DRY não se aplica
DRY tem um limite: não confunda duplicação com coincidência. Se dois trechos parecem iguais agora mas têm razões diferentes para mudar, não una-os.
// ❌ Falso DRY — "formatar nome de usuário" e "formatar nome de produto"
// Parecem a mesma coisa, mas regras de negócio são diferentes
function formatName(name: string) {
return name.trim().toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
}
// Usuário: "da Silva" → "Da Silva" (preposição minúscula)
// Produto: "da Silva" → "Da Silva" (tudo capitalizado)
// ✅ Separado — cada um tem sua regra
function formatUserName(name: string) { /* regras para nomes */ }
function formatProductName(name: string) { /* regras para produtos */ }KISS — Keep It Simple, Stupid
"A simplicidade deve ser um objetivo de design, não um subproduto." — Kelly Johnson (Engenheiro da Lockheed Martin, 1960)
O que é
Se a solução parece complexa, provavelmente existe uma mais simples. Código simples é mais fácil de ler, testar e manter.
Frontend — Hook complexo vs simples
// ❌ Over-engineered — genérico demais para o caso de uso
function useGenericDataFetcher<T, E, F>(
url: string,
options: FetchOptions<T, E, F>,
transformers: TransformerChain<T, E, F>,
validators: ValidatorSet<T>,
errorHandler: ErrorHandler<E>,
cacheStrategy: CacheStrategy<T>,
) {
// 200 linhas de lógica genérica que ninguém entende
}
// Chamada — precisa de 30 linhas de configuração
useGenericDataFetcher<User, ApiError, UserDTO>(
'/api/users/1',
{ method: 'GET', headers: { /* ... */ } },
{ toDTO: transformUserToDTO, fromDTO: transformDTOToUser },
{ schema: userSchema, strict: true },
{ fallback: defaultErrorHandler, retry: 3 },
{ ttl: 3600, staleWhileRevalidate: true },
);// ✅ Simples — faz o que precisa, nada mais
function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
}
// Chamada — 1 linha
const { data: user, isLoading } = useUser('1');Backend — Controller complexo vs simples
// ❌ Over-engineered — 50 linhas para retornar um JSON
class UserController {
async getUser(req: Request, res: Response) {
const { id } = req.params;
const validator = new UserValidator();
const validation = await validator.validateGetRequest(req);
if (!validation.isValid) {
const errorResponse = ResponseFactory.createError(400, validation.errors);
Logger.logError('UserController.getUser', validation.errors);
return res.status(400).json(errorResponse);
}
const sanitizedId = Sanitizer.sanitizeString(id);
const service = ServiceFactory.createUserService();
const result = await service.getUserById(sanitizedId);
if (result.isLeft()) {
const mappedError = ErrorMapper.map(result.value);
Logger.logError('UserController.getUser', mappedError);
return res.status(mappedError.statusCode).json(mappedError);
}
const dto = DTOFactory.createUserDTO(result.value);
const response = ResponseFactory.createSuccess(dto);
Logger.logInfo('UserController.getUser', { id: sanitizedId });
return res.status(200).json(response);
}
}// ✅ Simples — claro, testável, curto
class UserController {
async getUser(req: Request, res: Response) {
const user = await this.userService.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
return res.json(user);
}
}YAGNI — You Aren't Gonna Need It
"Sempre implemente as coisas quando você realmente precisa delas, nunca quando apenas prevê que vai precisar." — Extreme Programming
O que é
Não adicione funcionalidade "por via das dúvidas". Se o requisito não existe hoje, não implemente. Código que não é usado é código que precisa ser mantido, testado e compreendido — sem nenhum benefício.
Frontend — Sistema de temas sem necessidade
// ❌ YAGNI violado — ninguém pediu dark mode, tema customizável ou i18n
interface ThemeConfig {
mode: 'light' | 'dark' | 'auto' | 'custom';
colors: {
primary: string;
secondary: string;
accent: string;
background: string;
surface: string;
error: string;
warning: string;
info: string;
success: string;
text: {
primary: string;
secondary: string;
disabled: string;
};
};
typography: { /* 20 propriedades */ };
spacing: { /* 10 propriedades */ };
breakpoints: { /* 5 propriedades */ };
shadows: { /* 6 níveis */ };
borders: { /* ... */ };
}
// 500 linhas de config de tema que ninguém usa
// O cliente quer apenas: fundo branco, texto preto, botão azul// ✅ YAGNI respeito — faz o que o cliente pediu
// styles.css — simples, funciona
// Se dark mode for necessário no futuro, aí sim implementaBackend — Sistema de plugins sem necessidade
// ❌ YAGNI violado — ninguém pediu sistema de plugins
interface Plugin {
name: string;
version: string;
hooks: {
beforeRequest: (req: Request) => Request;
afterResponse: (res: Response) => Response;
onError: (error: Error) => void;
};
config: Record<string, unknown>;
}
class PluginManager {
private plugins: Map<string, Plugin> = new Map();
register(plugin: Plugin): void { /* ... */ }
unregister(name: string): void { /* ... */ }
getPlugin(name: string): Plugin | undefined { /* ... */ }
runHook(hook: string, data: any): any { /* ... */ }
getMiddleware(): Middleware { /* ... */ }
// 200 linhas de sistema de plugins...
}
// O sistema tem 3 meses e 0 plugins instalados// ✅ YAGNI respeito — quando precisar de middleware, adiciona diretamente
app.use(cors());
app.use(helmet());
app.use(rateLimit());
// Pronto. Se um dia precisar de plugins, aí implementa.Os três juntos na prática
// ❌ Viola os 3 princípios
// DRY: validação duplicada em cada rota
// KISS: lógica desnecessariamente complexa
// YAGNI: sistema de cache genérico que ninguém usa
class DataProcessor {
private cache = new LRUCache<string, any>({ max: 1000, ttl: 3600000 });
private validator = new ComplexValidator();
private transformer = new GenericTransformer();
async process(data: any) {
const cacheKey = this.generateCacheKey(data);
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
const validated = await this.validator.validate(data, { deep: true, strict: true });
const transformed = await this.transformer.transform(validated, {
normalize: true,
deduplicate: true,
enrich: true,
});
await this.cache.set(cacheKey, transformed);
return transformed;
}
}// ✅ Respeita os 3
// DRY: validação extraída em função reutilizável
// KISS: faz o que precisa, sem genéricos
// YAGNI: sem cache que ninguém pediu
function validateOrder(data: unknown): Result<Order, ValidationError> {
if (!data || typeof data !== 'object') {
return { ok: false, error: new ValidationError('Dados inválidos') };
}
if (!('productId' in data) || typeof data.productId !== 'string') {
return { ok: false, error: new ValidationError('productId é obrigatório') };
}
if (!('quantity' in data) || typeof data.quantity !== 'number' || data.quantity < 1) {
return { ok: false, error: new ValidationError('quantity deve ser >= 1') };
}
return { ok: true, value: data as Order };
}
async function createOrder(data: unknown) {
const result = validateOrder(data);
if (!result.ok) return result;
return ordersRepository.create(result.value);
}