DRY Principle
What is DRY?
DRY, which stands for "Don't Repeat Yourself," is a fundamental principle of software development aimed at reducing repetition in code. The principle was formulated by Andy Hunt and Dave Thomas in their book "The Pragmatic Programmer" and is stated as:
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
The essence of DRY is to avoid duplicating code, logic, or data across a system. When the same information is expressed in multiple places, changes require updates in multiple locations, increasing the chance of inconsistencies and errors.
The DRY Principle
Why DRY Matters
Reduced Maintenance Burden
When logic exists in only one place, updates require changes to only that single location. This drastically reduces the maintenance effort and the risk of overlooking update locations.
Improved Consistency
With a single source of truth, you eliminate the risk of inconsistent implementations or contradictory logic across the codebase.
Enhanced Code Quality
DRY code tends to be more organized and modular. The process of eliminating duplication often leads to better abstractions and cleaner architecture.
Better Scalability
As your application grows, DRY principles help manage complexity by keeping the codebase size proportional to its functionality rather than its repetition.
Easier Testing
With functionality consolidated in one place, you can focus testing efforts on that single implementation rather than testing duplicate code.
DRY in Practice
Example 1: Constants Management
Consider a web application with error messages repeated throughout the code:
Centralizing Error Messages
Moving repeated strings to a central constants file improves maintainability and consistency
// Without DRY - error messages repeated in multiple places
function validateEmail(email) {
if (!email.includes('@')) {
return "Please enter a valid email address.";
}
return null;
}
function registerUser(user) {
if (!user.email.includes('@')) {
throw new Error("Please enter a valid email address.");
}
// Registration logic
}
function updateUserEmail(userId, newEmail) {
if (!newEmail.includes('@')) {
return {
success: false,
error: "Please enter a valid email address."
};
}
// Update logic
}
// With DRY - centralized error messages
const ErrorMessages = {
INVALID_EMAIL: "Please enter a valid email address.",
USERNAME_TAKEN: "This username is already taken.",
PASSWORD_TOO_SHORT: "Password must be at least 8 characters long.",
UNAUTHORIZED: "You do not have permission to access this resource."
};
function validateEmail(email) {
if (!email.includes('@')) {
return ErrorMessages.INVALID_EMAIL;
}
return null;
}
function registerUser(user) {
if (!user.email.includes('@')) {
throw new Error(ErrorMessages.INVALID_EMAIL);
}
// Registration logic
}
function updateUserEmail(userId, newEmail) {
if (!newEmail.includes('@')) {
return {
success: false,
error: ErrorMessages.INVALID_EMAIL
};
}
// Update logic
}
This approach:
- Centralizes all error messages in one place
- Makes updates to message text consistent across the application
- Allows for easier localization/internationalization
- Improves code readability by using meaningful constants
Example 2: Shared Validation Logic
Consider validation logic that's used across multiple components:
Extracting Common Validation Logic
Creating reusable validation functions eliminates duplication across forms
// Without DRY - repeated validation logic
function UserForm() {
function validateForm(formData) {
if (!formData.name.trim()) {
return "Name is required";
}
if (!formData.email.includes('@')) {
return "Invalid email format";
}
if (formData.password.length < 8) {
return "Password must be at least 8 characters";
}
return null;
}
// Form handling logic
}
function AdminUserForm() {
function validateForm(formData) {
if (!formData.name.trim()) {
return "Name is required";
}
if (!formData.email.includes('@')) {
return "Invalid email format";
}
if (formData.password.length < 8) {
return "Password must be at least 8 characters";
}
if (!formData.role) {
return "Role is required";
}
return null;
}
// Form handling logic
}
// With DRY - centralized validation logic
const UserValidation = {
validateName(name) {
return name.trim() ? null : "Name is required";
},
validateEmail(email) {
return email.includes('@') ? null : "Invalid email format";
},
validatePassword(password) {
return password.length >= 8 ? null : "Password must be at least 8 characters";
}
};
function UserForm() {
function validateForm(formData) {
return UserValidation.validateName(formData.name) ||
UserValidation.validateEmail(formData.email) ||
UserValidation.validatePassword(formData.password) ||
null;
}
// Form handling logic
}
function AdminUserForm() {
function validateForm(formData) {
return UserValidation.validateName(formData.name) ||
UserValidation.validateEmail(formData.email) ||
UserValidation.validatePassword(formData.password) ||
(formData.role ? null : "Role is required");
}
// Form handling logic
}
This approach:
- Centralizes validation rules in one location
- Makes validation consistent across different forms
- Allows for easier updates to validation logic
- Supports composition of validation rules for different contexts
Example 3: Helper Functions
Consider formatting functions used across an application:
Creating Shared Utility Functions
Moving common formatting logic to a utilities file promotes reuse and consistency
// Without DRY - repeated formatting logic
function UserProfile({ user }) {
function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
return (
<div>
<h1>{user.name}</h1>
<p>Member since: {formatDate(user.joinDate)}</p>
</div>
);
}
function ActivityLog({ activities }) {
function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
return (
<div>
<h2>Recent Activity</h2>
<ul>
{activities.map(activity => (
<li key={activity.id}>
{activity.description} - {formatDate(activity.date)}
</li>
))}
</ul>
</div>
);
}
// With DRY - centralized formatting functions
// utils/formatters.js
export function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
export function formatPhoneNumber(phone) {
// Format phone number logic
}
// Components using the shared utility
import { formatDate } from '../utils/formatters';
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>Member since: {formatDate(user.joinDate)}</p>
</div>
);
}
function ActivityLog({ activities }) {
return (
<div>
<h2>Recent Activity</h2>
<ul>
{activities.map(activity => (
<li key={activity.id}>
{activity.description} - {formatDate(activity.date)}
</li>
))}
</ul>
</div>
);
}
This approach:
- Centralizes formatting logic in a dedicated utilities file
- Ensures consistent formatting across the application
- Makes it easy to update formatting logic in one place
- Improves code organization and reusability
Applying DRY in Your Code
Do
- Extract common logic: Move repeated code into functions or classes
- Use constants: Define constants for values used in multiple places
- Create helper modules: Organize shared functionality in dedicated modules
- Use inheritance appropriately: Share functionality through class inheritance
- Apply composition: Use object composition to reuse behavior
Don't
- Copy-paste code: Avoid duplicating code between different parts of the application
- Define the same logic multiple times: Consolidate logic into a single location
- Hardcode values: Replace hardcoded values with named constants
- Create redundant documentation: Ensure documentation is synchronized with code
- Implement parallel hierarchies: Avoid creating multiple class hierarchies that mirror each other
Finding and Fixing DRY Violations
Identifying DRY Violations
Signs that you might be violating DRY include:
- Copy-pasted code: Blocks of code that are identical or nearly identical
- Similar functions: Functions that perform almost the same task with minor variations
- Repeated string literals: The same text appearing in multiple places
- Duplicated validation rules: The same validation logic implemented in multiple places
- Parallel updates: Having to make the same change in multiple places
Techniques for Eliminating Duplication
1. Extract Method
Move duplicated code into a separate method that can be called from multiple places.
// Before extraction
function processUser(user) {
// 20 lines of validation logic
// Save user
database.save(user);
}
function processAdmin(admin) {
// Same 20 lines of validation logic
// Save admin with special handling
admin.role = 'admin';
database.save(admin);
}
// After extraction
function validatePerson(person) {
// 20 lines of validation logic
}
function processUser(user) {
validatePerson(user);
database.save(user);
}
function processAdmin(admin) {
validatePerson(admin);
admin.role = 'admin';
database.save(admin);
}
2. Template Method Pattern
Define a skeleton of an algorithm in a method, deferring some steps to subclasses.
// Using the Template Method pattern
class DocumentProcessor {
process(document) {
this.validate(document);
this.preprocess(document);
this.save(document);
this.postprocess(document);
}
validate(document) {
// Common validation logic
}
preprocess(document) {
// To be overridden by subclasses
}
save(document) {
// Common saving logic
}
postprocess(document) {
// To be overridden by subclasses
}
}
class InvoiceProcessor extends DocumentProcessor {
preprocess(document) {
// Invoice-specific preprocessing
}
postprocess(document) {
// Invoice-specific postprocessing
}
}
class ContractProcessor extends DocumentProcessor {
preprocess(document) {
// Contract-specific preprocessing
}
postprocess(document) {
// Contract-specific postprocessing
}
}
3. Strategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable.
// Using the Strategy pattern
class PaymentProcessor {
constructor(paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
processPayment(amount) {
return this.paymentStrategy.process(amount);
}
}
class CreditCardPayment {
process(amount) {
console.log(`Processing credit card payment of ${amount}`);
// Credit card processing logic
}
}
class PayPalPayment {
process(amount) {
console.log(`Processing PayPal payment of ${amount}`);
// PayPal processing logic
}
}
class BitcoinPayment {
process(amount) {
console.log(`Processing Bitcoin payment of ${amount}`);
// Bitcoin processing logic
}
}
// Usage
const processor = new PaymentProcessor(new CreditCardPayment());
processor.processPayment(100);
// Change strategy at runtime
processor.paymentStrategy = new PayPalPayment();
processor.processPayment(100);
4. Constants and Enumerations
Use constants or enums for values that appear multiple times.
// Using constants
const HttpStatus = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500
};
function handleResponse(response) {
switch (response.status) {
case HttpStatus.OK:
return processSuccess(response);
case HttpStatus.UNAUTHORIZED:
return redirectToLogin();
case HttpStatus.FORBIDDEN:
return showPermissionError();
case HttpStatus.NOT_FOUND:
return showNotFoundError();
default:
return showGenericError();
}
}
5. Configuration Files
Move repeated configuration values to external configuration files.
// config.js
export default {
apiUrl: 'https://api.example.com',
maxRetries: 3,
timeout: 5000,
pageSize: 20,
dateFormat: 'YYYY-MM-DD',
supportEmail: 'support@example.com'
};
// Usage in components
import config from './config';
function fetchData() {
return fetch(`${config.apiUrl}/data`, {
timeout: config.timeout
});
}
function showError() {
return `Contact ${config.supportEmail} for assistance`;
}
Balancing DRY with Pragmatism
While DRY is a powerful principle, it should be applied pragmatically:
When to Apply DRY
- When the same logic appears in multiple places
- When changes would require updates in multiple locations
- When abstractions improve code clarity and maintainability
When DRY Might Not Apply
- When creating an abstraction would be more complex than the duplication
- When seemingly similar code might evolve differently
- When the cost of the abstraction outweighs the benefit
Sometimes it's better to repeat code once before creating an abstraction. The "Write Everything Twice" (WET) approach suggests waiting until you have at least two instances of duplication before refactoring, ensuring your abstraction solves a real problem.
Advanced DRY Techniques
Higher-Order Functions
Use higher-order functions to abstract common patterns:
// Without higher-order functions
function validateEmail(email) {
if (!email) return 'Email is required';
return null;
}
function validatePassword(password) {
if (!password) return 'Password is required';
return null;
}
function validateUsername(username) {
if (!username) return 'Username is required';
return null;
}
// With higher-order functions
function required(fieldName) {
return function(value) {
if (!value) return `${fieldName} is required`;
return null;
};
}
const validateEmail = required('Email');
const validatePassword = required('Password');
const validateUsername = required('Username');
Middleware Patterns
Use middleware patterns to create reusable processing chains:
// Middleware pattern for request processing
function createRequestProcessor(middlewares) {
return async function processRequest(request) {
let index = 0;
async function next() {
if (index >= middlewares.length) {
return;
}
const middleware = middlewares[index++];
await middleware(request, next);
}
await next();
return request;
};
}
// Middleware functions
async function authenticate(request, next) {
console.log('Authenticating request');
// Authentication logic
await next();
}
async function authorize(request, next) {
console.log('Authorizing request');
// Authorization logic
await next();
}
async function validate(request, next) {
console.log('Validating request');
// Validation logic
await next();
}
async function processData(request, next) {
console.log('Processing data');
// Data processing logic
await next();
}
// Create processor with middleware chain
const processRequest = createRequestProcessor([
authenticate,
authorize,
validate,
processData
]);
// Usage
processRequest({ url: '/api/data', body: { /* data */ } });
Code Generation
For extreme cases of repetition, consider code generation:
// Dynamic function creation for API endpoints
function createApiEndpoints(entityNames) {
const api = {};
for (const entity of entityNames) {
const capitalizedEntity = entity.charAt(0).toUpperCase() + entity.slice(1);
api[`get${capitalizedEntity}`] = async (id) => {
return fetch(`/api/${entity}/${id}`).then(res => res.json());
};
api[`create${capitalizedEntity}`] = async (data) => {
return fetch(`/api/${entity}`, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
};
api[`update${capitalizedEntity}`] = async (id, data) => {
return fetch(`/api/${entity}/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
};
api[`delete${capitalizedEntity}`] = async (id) => {
return fetch(`/api/${entity}/${id}`, {
method: 'DELETE'
}).then(res => res.json());
};
}
return api;
}
// Usage
const api = createApiEndpoints(['user', 'product', 'order']);
api.getUser(1);
api.createProduct({ name: 'New Product', price: 29.99 });
Conclusion
DRY is a foundational principle that helps create maintainable, consistent codebases. By eliminating duplication, developers can make code more robust, easier to update, and less prone to inconsistencies.
Remember that DRY is not about eliminating all repetition, but about ensuring that each piece of knowledge has a single, authoritative representation. Applied with pragmatism, DRY leads to more maintainable, flexible, and elegant code.
When you find yourself copying and pasting code, stop and ask: "Should this be abstracted into a reusable component?" The answer will guide you toward cleaner, more maintainable code.