Added AI skills
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
---
|
||||
id: core-dry-extraction
|
||||
title: DRY - Code Extraction
|
||||
category: core-principles
|
||||
priority: critical
|
||||
tags: [DRY, refactoring, extraction, code-reuse]
|
||||
related: [core-dry, core-dry-single-source, solid-srp-function]
|
||||
---
|
||||
|
||||
# DRY - Code Extraction
|
||||
|
||||
Don't Repeat Yourself. When you find duplicated code, extract it into a reusable function, method, or module. Every piece of knowledge should have a single, unambiguous representation.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```typescript
|
||||
// Anti-pattern: Same validation logic repeated in multiple places
|
||||
|
||||
class UserController {
|
||||
async createUser(req: Request, res: Response): Promise<void> {
|
||||
const { email, password, name } = req.body;
|
||||
|
||||
// Email validation - duplicated
|
||||
if (!email) {
|
||||
res.status(400).json({ error: 'Email is required' });
|
||||
return;
|
||||
}
|
||||
if (!email.includes('@') || !email.includes('.')) {
|
||||
res.status(400).json({ error: 'Invalid email format' });
|
||||
return;
|
||||
}
|
||||
if (email.length > 255) {
|
||||
res.status(400).json({ error: 'Email too long' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Password validation - duplicated
|
||||
if (!password) {
|
||||
res.status(400).json({ error: 'Password is required' });
|
||||
return;
|
||||
}
|
||||
if (password.length < 8) {
|
||||
res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||
return;
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
res.status(400).json({ error: 'Password must contain uppercase letter' });
|
||||
return;
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
res.status(400).json({ error: 'Password must contain a number' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create user...
|
||||
}
|
||||
|
||||
async updateUser(req: Request, res: Response): Promise<void> {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Same email validation repeated
|
||||
if (email) {
|
||||
if (!email.includes('@') || !email.includes('.')) {
|
||||
res.status(400).json({ error: 'Invalid email format' });
|
||||
return;
|
||||
}
|
||||
if (email.length > 255) {
|
||||
res.status(400).json({ error: 'Email too long' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Same password validation repeated
|
||||
if (password) {
|
||||
if (password.length < 8) {
|
||||
res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||
return;
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
res.status(400).json({ error: 'Password must contain uppercase letter' });
|
||||
return;
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
res.status(400).json({ error: 'Password must contain a number' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update user...
|
||||
}
|
||||
|
||||
async resetPassword(req: Request, res: Response): Promise<void> {
|
||||
const { email, newPassword } = req.body;
|
||||
|
||||
// Email validation repeated again
|
||||
if (!email) {
|
||||
res.status(400).json({ error: 'Email is required' });
|
||||
return;
|
||||
}
|
||||
if (!email.includes('@') || !email.includes('.')) {
|
||||
res.status(400).json({ error: 'Invalid email format' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Password validation repeated again
|
||||
if (!newPassword) {
|
||||
res.status(400).json({ error: 'New password is required' });
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||
return;
|
||||
}
|
||||
if (!/[A-Z]/.test(newPassword)) {
|
||||
res.status(400).json({ error: 'Password must contain uppercase letter' });
|
||||
return;
|
||||
}
|
||||
if (!/[0-9]/.test(newPassword)) {
|
||||
res.status(400).json({ error: 'Password must contain a number' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset password...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```typescript
|
||||
// Correct approach: Extract reusable validation functions
|
||||
|
||||
// Validation result type
|
||||
interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// Reusable validation functions
|
||||
class Validators {
|
||||
static email(email: string | undefined, options: { required?: boolean } = {}): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!email) {
|
||||
if (options.required) {
|
||||
errors.push('Email is required');
|
||||
}
|
||||
return { isValid: !options.required, errors };
|
||||
}
|
||||
|
||||
if (!email.includes('@') || !email.includes('.')) {
|
||||
errors.push('Invalid email format');
|
||||
}
|
||||
|
||||
if (email.length > 255) {
|
||||
errors.push('Email must be 255 characters or less');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
static password(password: string | undefined, options: { required?: boolean } = {}): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!password) {
|
||||
if (options.required) {
|
||||
errors.push('Password is required');
|
||||
}
|
||||
return { isValid: !options.required, errors };
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.push('Password must be at least 8 characters');
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push('Password must contain at least one uppercase letter');
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push('Password must contain at least one lowercase letter');
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(password)) {
|
||||
errors.push('Password must contain at least one number');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
static combine(...results: ValidationResult[]): ValidationResult {
|
||||
const errors = results.flatMap(r => r.errors);
|
||||
return { isValid: errors.length === 0, errors };
|
||||
}
|
||||
}
|
||||
|
||||
// Reusable error response helper
|
||||
function validationError(res: Response, errors: string[]): void {
|
||||
res.status(400).json({ errors });
|
||||
}
|
||||
|
||||
// Clean controller using extracted validations
|
||||
class UserController {
|
||||
async createUser(req: Request, res: Response): Promise<void> {
|
||||
const { email, password, name } = req.body;
|
||||
|
||||
const validation = Validators.combine(
|
||||
Validators.email(email, { required: true }),
|
||||
Validators.password(password, { required: true })
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
return validationError(res, validation.errors);
|
||||
}
|
||||
|
||||
// Create user...
|
||||
}
|
||||
|
||||
async updateUser(req: Request, res: Response): Promise<void> {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const validation = Validators.combine(
|
||||
Validators.email(email),
|
||||
Validators.password(password)
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
return validationError(res, validation.errors);
|
||||
}
|
||||
|
||||
// Update user...
|
||||
}
|
||||
|
||||
async resetPassword(req: Request, res: Response): Promise<void> {
|
||||
const { email, newPassword } = req.body;
|
||||
|
||||
const validation = Validators.combine(
|
||||
Validators.email(email, { required: true }),
|
||||
Validators.password(newPassword, { required: true })
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
return validationError(res, validation.errors);
|
||||
}
|
||||
|
||||
// Reset password...
|
||||
}
|
||||
}
|
||||
|
||||
// Validators can be reused across the application
|
||||
class AdminController {
|
||||
async inviteUser(req: Request, res: Response): Promise<void> {
|
||||
const { email } = req.body;
|
||||
|
||||
const validation = Validators.email(email, { required: true });
|
||||
|
||||
if (!validation.isValid) {
|
||||
return validationError(res, validation.errors);
|
||||
}
|
||||
|
||||
// Send invite...
|
||||
}
|
||||
}
|
||||
|
||||
// Easy to test in isolation
|
||||
describe('Validators', () => {
|
||||
describe('email', () => {
|
||||
it('should reject invalid email format', () => {
|
||||
const result = Validators.email('invalid');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Invalid email format');
|
||||
});
|
||||
|
||||
it('should accept valid email', () => {
|
||||
const result = Validators.email('user@example.com');
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should require email when required option is set', () => {
|
||||
const result = Validators.email(undefined, { required: true });
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Email is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('password', () => {
|
||||
it('should reject short passwords', () => {
|
||||
const result = Validators.password('short');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must be at least 8 characters');
|
||||
});
|
||||
|
||||
it('should require uppercase letter', () => {
|
||||
const result = Validators.password('lowercase123');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must contain at least one uppercase letter');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
1. **Single Source of Truth**: Password rules are defined once. Change them in one place, and all usages are updated.
|
||||
|
||||
2. **Consistency**: All email validations behave the same way. No risk of inconsistent error messages or rules.
|
||||
|
||||
3. **Easier Testing**: Test the validation logic once, thoroughly. No need to test the same logic in every controller.
|
||||
|
||||
4. **Bug Fixes Propagate**: Fix a bug in `Validators.email()`, and it's fixed everywhere.
|
||||
|
||||
5. **Reduced Code Size**: Less code means less to read, less to maintain, and fewer places for bugs.
|
||||
|
||||
6. **Better Abstraction**: Controllers focus on HTTP concerns, validators focus on validation.
|
||||
|
||||
7. **Reusability**: Same validators work in controllers, services, CLI tools, or anywhere else.
|
||||
Reference in New Issue
Block a user