joabgonzalez

dry-principle

"Eliminate knowledge duplication via abstraction. Trigger: When same logic appears 3+ times or changing one place requires updating others."

joabgonzalez 5 Updated 3mo ago

Resources

1
GitHub

Install

npx skillscat add joabgonzalez/ai-agents-skills/dry-principle

Install via the SkillsCat registry.

SKILL.md

DRY Principle

Eliminate knowledge duplication through abstraction. DRY is about knowledge duplication, not code duplication—similar-looking code representing different concepts should NOT be merged.

When to Use

  • Same logic appears in 3+ places (Rule of Three)
  • Changing a rule requires updating multiple files
  • Shared configuration or constants duplicated across modules

Don't use for:

  • Code that looks similar but represents different business concepts
  • Premature abstraction (apply after seeing duplication, not speculatively)
  • Trivial one-off operations

Critical Patterns

✅ REQUIRED: Rule of Three — Extract After 3rd Occurrence

Apply DRY when logic appears in 3+ places. Below that, duplication may be acceptable.

// ❌ WRONG: validateEmail duplicated in RegistrationForm, LoginForm, ProfileForm
const validateEmail = (email: string) => {
  if (!email.includes('@')) return 'Invalid email';
  if (email.length < 5) return 'Email too short';
  return null;
};

// ✅ CORRECT: Extracted to shared utility
// utils/validation.ts
export const validateEmail = (email: string): string | null => {
  if (!email.includes('@')) return 'Invalid email';
  if (email.length < 5) return 'Email too short';
  return null;
};

✅ REQUIRED: Centralize Configuration

Single source of truth for constants, endpoints, and config values.

// ❌ WRONG: Same URL in 5 different files
const BASE_URL = 'https://api.example.com/v1';  // user.service.ts
const API_URL = 'https://api.example.com/v1';   // order.service.ts

// ✅ CORRECT: One source
// config/api.ts
export const API_BASE_URL = 'https://api.example.com/v1';
export const API_ENDPOINTS = {
  users: `${API_BASE_URL}/users`,
  orders: `${API_BASE_URL}/orders`,
};

✅ REQUIRED: Shared Types

Define types once, import everywhere.

// ❌ WRONG: User type defined in 3 files with slight variations
// users.service.ts
type User = { id: string; email: string; name: string };
// auth.service.ts
type User = { id: string; email: string; role: string }; // different!

// ✅ CORRECT: Single definition
// types/user.ts
export interface User { id: string; email: string; name: string; role: string; }

❌ NEVER: Merge Code That Looks Similar but Isn't

DRY is about knowledge duplication, not code similarity.

// ❌ WRONG: Merged because they look similar (but represent different concepts)
function applyDiscount(price: number, percent: number) { return price * (1 - percent / 100); }
// Used for: loyalty discount AND promotional discount AND employee discount
// Problem: Different business rules → now tangled

// ✅ CORRECT: Different concepts stay separate even if they look similar
function applyLoyaltyDiscount(price: number, loyaltyPercent: number): number { ... }
function applyPromoDiscount(price: number, promoPercent: number): number { ... }

Decision Tree

Logic appears in 3+ places?
  → YES: Extract to shared function/utility/constant
  → NO (2 places): Consider if it's worth extracting; often OK to duplicate

Code looks similar but represents different business concepts?
  → Extract = premature abstraction → Keep separate

Configuration duplicated across files?
  → Move to centralized config module

Types defined multiple times with variations?
  → Define once in types/ directory, import everywhere

Abstraction would require many parameters or conditions?
  → Abstraction is fighting the code → may not be a real duplication

Example

// ✅ CORRECT: Full DRY example
// config/api.ts — single source for URLs
export const ENDPOINTS = { users: '/api/users', orders: '/api/orders' };

// types/pagination.ts — shared pagination type
export interface PaginatedResponse<T> { data: T[]; total: number; page: number; }

// utils/http.ts — shared fetch wrapper
export async function fetchJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

// services/user.service.ts — uses shared utilities
import { ENDPOINTS } from '../config/api';
import { fetchJson } from '../utils/http';
import type { PaginatedResponse } from '../types/pagination';
import type { User } from '../types/user';

export const getUsers = () => fetchJson<PaginatedResponse<User>>(ENDPOINTS.users);

Edge Cases

Forced parameter proliferation: If your shared function needs 5+ parameters to handle all callers, DRY may be wrong. The callers may represent genuinely different concerns.

Test helpers: Same test setup code in multiple tests — extract to beforeEach or test fixtures. This is legitimate DRY in tests.

Accidental coupling: Merging two functions because they look similar can accidentally couple unrelated business rules. If they change independently, keep them separate.


Resources