enforce-modularity
Apply Single Responsibility Principle when writing code. Keep modules focused, cohesive, and loosely coupled. Guide splitting classes with too many methods. Ensure code is reusable and testable.
When & Why to Use This Skill
This Claude skill enforces high-quality software architecture by applying the Single Responsibility Principle (SRP) and modularity standards. It guides developers in creating focused, cohesive, and loosely coupled modules, ensuring codebases remain maintainable, testable, and scalable while preventing the accumulation of monolithic technical debt.
Use Cases
- Refactoring Monolithic Classes: Identify and split 'God Objects' or classes with excessive methods into smaller, focused services that are easier to manage.
- Architectural Planning: Design new features using a feature-based structure to ensure high cohesion and low coupling from the initial development phase.
- Automated Code Quality Auditing: Enforce strict metrics such as maximum lines per function, method counts per class, and parameter limits to maintain clean code standards.
- Decoupling Dependencies: Improve system flexibility by replacing direct concrete class dependencies with interface-based abstractions, facilitating easier implementation swaps.
- Promoting Code Reusability: Systematically extract duplicated logic into shared utilities or core modules to adhere to the DRY (Don't Repeat Yourself) principle.
| name | enforce-modularity |
|---|---|
| description | Apply Single Responsibility Principle when writing code. Keep modules focused, cohesive, and loosely coupled. Guide splitting classes with too many methods. Ensure code is reusable and testable. |
Modularity Standards
CRITICAL: Write modular, focused, maintainable code. Each module should do ONE thing well.
Why This Exists
Monolithic code:
- Hard to understand
- Hard to test
- Hard to maintain
- Hard to reuse
- Easy to break
Modular code is easier to understand, test, maintain, reuse, and evolve.
When to Apply
Apply modularity when:
- Creating new files/classes/functions
- Refactoring existing code
- Reviewing code quality
- Planning architecture
How It Works
1. Single Responsibility Principle (SRP)
Each file, class, and function should do ONE thing well.
File Level
- Each file represents ONE concept/responsibility
- File name clearly indicates single purpose
- If you can't name it without "and" or "or", split it
Class Level
- Each class has ONE reason to change
- Maximum 7-10 public methods per class
- Private methods don't count toward limit
Function Level
- Each function does ONE task
- Maximum 20-30 lines per function
- Maximum 3-4 parameters per function
- If needs many params, group into object
2. High Cohesion
Functions in a module work on related data.
Good cohesion:
- ✅ All code in file serves module's single purpose
- ✅ Functions work on same data structures
- ✅ Changes to one part don't cascade
Bad cohesion:
- ❌ Unrelated functions grouped together
- ❌ File does multiple unrelated things
- ❌ Changes require editing multiple sections
3. Low Coupling
Modules depend on interfaces, not implementations.
Good coupling:
- ✅ Depend on abstractions (interfaces)
- ✅ Easy to swap implementations
- ✅ Changes don't cascade to other modules
Bad coupling:
- ❌ Direct dependencies on concrete classes
- ❌ Hard to test in isolation
- ❌ Changes break other modules
Examples
Good: Single Responsibility Per Service
// ✅ UserService.ts - User management only
class UserService {
createUser() {}
updateUser() {}
deleteUser() {}
findUserById() {}
}
// ✅ EmailService.ts - Email only
class EmailService {
sendWelcomeEmail() {}
sendPasswordResetEmail() {}
sendNotification() {}
}
// ✅ TokenService.ts - Tokens only
class TokenService {
generateToken() {}
verifyToken() {}
refreshToken() {}
}
Bad: Multiple Responsibilities
// ❌ UserService.ts - Too many responsibilities!
class UserService {
// User management
createUser() {}
updateUser() {}
// Email (should be EmailService)
sendEmail() {}
// Logging (should be Logger)
logActivity() {}
// Validation (should be Validator)
validatePassword() {}
// Auth (should be TokenService)
generateToken() {}
}
Good: High Cohesion, Low Coupling
// ✅ Interface for abstraction
interface IEmailProvider {
send(to: string, subject: string, body: string): Promise<void>;
}
// ✅ Service depends on interface, not concrete implementation
class EmailService {
constructor(private provider: IEmailProvider) {}
async sendWelcomeEmail(user: User) {
await this.provider.send(
user.email,
'Welcome!',
this.buildWelcomeBody(user)
);
}
// All email logic together (high cohesion)
private buildWelcomeBody(user: User): string {
return `Welcome ${user.name}!`;
}
}
// ✅ Provider can be swapped without changing EmailService (low coupling)
class SendGridProvider implements IEmailProvider {
async send(to: string, subject: string, body: string) {
// SendGrid implementation
}
}
class MailgunProvider implements IEmailProvider {
async send(to: string, subject: string, body: string) {
// Mailgun implementation
}
}
Good: Testable in Isolation
// ✅ Pure, testable function
class PasswordValidator {
validateStrength(password: string): boolean {
return (
password.length >= 8 &&
/[A-Z]/.test(password) &&
/[0-9]/.test(password)
);
}
}
// Test is simple
test('validates strong password', () => {
const validator = new PasswordValidator();
expect(validator.validateStrength('Password123')).toBe(true);
});
Bad: Hard to Test
// ❌ Tightly coupled to framework, database, everything
class UserController {
createUser(req: Request, res: Response) {
// Validation mixed in
if (!req.body.email) {
return res.status(400).json({ error: 'Email required' });
}
// Database access mixed in
const user = await db.users.create(req.body);
// Email sending mixed in
await sendgrid.send({
to: user.email,
subject: 'Welcome'
});
// Response handling mixed in
res.json(user);
}
}
// Test requires mocking: HTTP framework, database, sendgrid, etc.
Module Organization
Feature-Based Structure
src/features/auth/
├── controllers/ # HTTP handlers (max 300 lines)
├── services/ # Business logic (max 300 lines)
├── repositories/ # Data access (max 300 lines)
├── models/ # Domain models (max 200 lines)
├── validators/ # Validation (max 150 lines)
├── types/ # TypeScript types (max 100 lines)
└── utils/ # Feature utilities (max 200 lines)
Shared vs Core
shared/ # Used by 2+ features
├── services/
├── utils/
├── types/
└── constants/
core/ # Infrastructure (rarely changes)
├── database/
├── config/
├── errors/
└── interfaces/
Code Reusability
Before writing ANY code:
Search for existing functionality
- Check
shared/utils/ - Look in related features
- Search for similar patterns
- Check
Extract common logic
- Duplicated in 2+ places? Extract it
- Function > 20 lines and reusable? Extract it
- Pattern could benefit others? Extract it
Choose location based on usage:
Used by ONE feature: feature/utils/ Used by 2+ features: shared/utils/ Used by ALL features: core/utils/
Example: Extracting Common Logic
// ❌ BEFORE: Duplicated in multiple files
// UserController.ts
async createUser(req, res) {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
// ...
}
// PostController.ts
async createPost(req, res) {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
// ...
}
// ✅ AFTER: Extracted to shared utility
// shared/utils/validation.ts
export function validateEmail(email: string): boolean {
return email && email.includes('@') && email.length > 3;
}
// Now used everywhere
import { validateEmail } from '@/shared/utils/validation';
if (!validateEmail(req.body.email)) {
return res.status(400).json({ error: 'Invalid email' });
}
Splitting Large Modules
When to Split
Split when:
- File approaching 400 lines
- Class has > 10 public methods
- Function > 30 lines
- Multiple responsibilities detected
- Hard to test in isolation
How to Split
Strategy 1: Extract Utilities
// ❌ BEFORE: UserService.ts (550 lines)
class UserService {
validateEmail() { /* 50 lines */ }
hashPassword() { /* 50 lines */ }
generateToken() { /* 50 lines */ }
createUser() { /* 400 lines */ }
}
// ✅ AFTER: Split utilities
// utils/emailValidation.ts
export function validateEmail() {}
// utils/passwordUtils.ts
export function hashPassword() {}
// utils/tokenGenerator.ts
export function generateToken() {}
// UserService.ts (400 lines)
import { validateEmail } from '../utils/emailValidation';
import { hashPassword } from '../utils/passwordUtils';
class UserService {
createUser() { /* uses imported utilities */ }
}
Strategy 2: Extract Sub-Services
// ❌ BEFORE: UserService.ts (600 lines)
class UserService {
// User CRUD (200 lines)
// Password management (200 lines)
// Profile management (200 lines)
}
// ✅ AFTER: Split by domain
// UserService.ts (200 lines)
class UserService {
createUser() {}
updateUser() {}
deleteUser() {}
}
// UserPasswordService.ts (200 lines)
class UserPasswordService {
changePassword() {}
resetPassword() {}
}
// UserProfileService.ts (200 lines)
class UserProfileService {
updateProfile() {}
uploadAvatar() {}
}
Enforcement
The code-quality-auditor will reject:
- ❌ Files > 500 lines
- ❌ Classes with > 10 public methods
- ❌ Functions > 30 lines
- ❌ Functions with > 4 parameters
- ❌ Multiple responsibilities in one file
- ❌ Duplicated code (DRY violations)
- ❌ Tight coupling to implementations
Quick Checklist
- Each file has single responsibility
- File name clearly describes purpose
- Classes have < 10 public methods
- Functions are < 30 lines
- Functions have < 4 parameters
- No code duplication
- Modules are loosely coupled (interfaces)
- Code is testable in isolation
- Reusable logic extracted to utilities