Skills

Install

$ npx ai-agents-skills add --skill dry-principle
Domain v1.0

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; }

✅ REQUIRED: React Custom Hooks as DRY Abstraction

Extract repeated fetch/state patterns into custom hooks — same Rule of Three applies.

// ❌ WRONG: identical fetch + state pattern in 3+ components
function UserList() {
  const [users, setUsers]     = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers).finally(() => setLoading(false)); }, []);
}
function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading]   = useState(true);
  useEffect(() => { fetch('/api/products').then(r => r.json()).then(setProducts).finally(() => setLoading(false)); }, []);
}

// ✅ CORRECT: extract once, parameterize the URL
function useFetch<T>(url: string) {
  const [data, setData]       = useState<T[]>([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => { fetch(url).then(r => r.json()).then(setData).finally(() => setLoading(false)); }, [url]);
  return { data, loading };
}

function UserList()    { const { data: users,    loading } = useFetch<User>('/api/users');    ... }
function ProductList() { const { data: products, loading } = useFetch<Product>('/api/products'); ... }

Rule: Apply only after seeing the same hook pattern in 3+ components — not speculatively.

❌ 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