- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction
- Category 1: Naming — 6 Rules
- Category 2: Functions — 6 Rules
- Category 3: Comments — 4 Rules
- Category 4: Error Handling — 4 Rules
- Category 5: Code Organization — 5 Rules
- Category 6: Testing — 5 Rules
- Code Smells Catalog
- Refactoring Techniques: Before/After
- Cognitive Complexity
- SOLID Principles Quick Reference
- Code Review Checklist
- Practical Quiz
- Tools and Automation
- References
Introduction
There is a seemingly insurmountable gap between "working code" and "readable code." Most developers focus on feature implementation and push code quality to the back burner, eventually watching maintenance costs snowball out of control.
Robert C. Martin's "Clean Code" was published 17 years ago, yet its principles remain as relevant as ever. However, with the emergence of modern languages and tools like TypeScript, Python, and Rust, the ways we practice those principles have evolved.
In this guide, we present 30 practical rules across 6 categories, each with Bad/Good code comparisons. All examples are in TypeScript and Python.
The Boy Scout Rule: Always leave the code cleaner than you found it.
Category 1: Naming — 6 Rules
Rule 1: Use Intention-Revealing Names
Variable, function, and class names should tell you "what it does" and "why it exists."
Bad:
const d: number = 86400;
const list1: number[] = [1, 2, 3];
function calc(a: number, b: number): number {
return a * b * 0.1;
}
Good:
const SECONDS_PER_DAY: number = 86400;
const activeUserIds: number[] = [1, 2, 3];
function calculateDiscountedPrice(originalPrice: number, quantity: number): number {
const DISCOUNT_RATE = 0.1;
return originalPrice * quantity * DISCOUNT_RATE;
}
Python version:
# Bad
d = 86400
lst = [1, 2, 3]
def calc(a, b):
return a * b * 0.1
# Good
SECONDS_PER_DAY = 86400
active_user_ids = [1, 2, 3]
def calculate_discounted_price(original_price: float, quantity: int) -> float:
DISCOUNT_RATE = 0.1
return original_price * quantity * DISCOUNT_RATE
Rule 2: Avoid Abbreviations
Abbreviations are clear only to the author and are puzzles for everyone else.
Bad:
const usrMgr = new UserManager();
const btnClkHndlr = () => {};
const genRpt = (dt: Date) => {};
Good:
const userManager = new UserManager();
const handleButtonClick = () => {};
const generateReport = (reportDate: Date) => {};
Acceptable abbreviations: Industry standards only — id, url, api, db, io, html, css, http
Rule 3: Use Searchable Names
Magic numbers and single-letter variables are impossible to search across a project.
Bad:
if (user.role === 5) {
setTimeout(callback, 86400000);
}
Good:
const ADMIN_ROLE_ID = 5;
const ONE_DAY_IN_MILLISECONDS = 86400000;
if (user.role === ADMIN_ROLE_ID) {
setTimeout(callback, ONE_DAY_IN_MILLISECONDS);
}
Rule 4: Use Pronounceable Names
You should be able to say names aloud during code reviews.
Bad:
class DtaRcrd102 {
private genymdhms: Date;
private modymdhms: Date;
private pszqint: number;
}
Good:
class Customer {
private generationTimestamp: Date;
private modificationTimestamp: Date;
private recordId: number;
}
Rule 5: Use Consistent Vocabulary
Do not mix multiple words for the same concept.
Bad:
class UserService {
fetchUser(id: number) {}
retrieveAccount(id: number) {}
getUserProfile(id: number) {}
obtainUserSettings(id: number) {}
}
Good:
class UserService {
getUser(id: number) {}
getAccount(id: number) {}
getUserProfile(id: number) {}
getUserSettings(id: number) {}
}
Team naming convention example:
| Action | Convention | Example |
|---|---|---|
| Read (single) | get + noun | getUser, getOrder |
| Read (list) | list + plural noun | listUsers, listOrders |
| Create | create + noun | createUser, createOrder |
| Update | update + noun | updateUser, updateOrder |
| Delete | delete + noun | deleteUser, deleteOrder |
| Check existence | has/is + adjective/noun | hasPermission, isActive |
| Convert | to + target form | toJSON, toString |
Rule 6: Name Length Should Match Scope
Short scopes get short names; wide scopes get descriptive names.
Bad:
// Long name for a loop variable
for (let arrayIndex = 0; arrayIndex < items.length; arrayIndex++) {
process(items[arrayIndex]);
}
// Short name for a global constant
const T = 3000;
Good:
// Loop variables are short
for (let i = 0; i < items.length; i++) {
process(items[i]);
}
// Global/module-level is descriptive
const API_REQUEST_TIMEOUT_MS = 3000;
Category 2: Functions — 6 Rules
Rule 7: Keep Functions Small
The ideal function length is under 20 lines. It should fit on one screen.
Bad:
function processOrder(order: Order): OrderResult {
// Validation (20 lines)
if (!order.items || order.items.length === 0) throw new Error('No items');
if (!order.customer) throw new Error('No customer');
// ... more validation
// Price calculation (30 lines)
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
const tax = total * 0.1;
const shipping = total > 100 ? 0 : 10;
// ... more calculations
// Payment processing (20 lines)
const paymentResult = chargeCard(order.customer.cardId, total + tax + shipping);
// ... payment logic
// Notification (15 lines)
sendEmail(order.customer.email, 'Order confirmed');
// ... notification logic
return { orderId: generateId(), total, status: 'completed' };
}
Good:
function processOrder(order: Order): OrderResult {
validateOrder(order);
const pricing = calculatePricing(order.items);
const paymentResult = processPayment(order.customer, pricing.total);
notifyCustomer(order.customer, paymentResult);
return createOrderResult(paymentResult, pricing);
}
function validateOrder(order: Order): void {
if (!order.items?.length) throw new InvalidOrderError('No items');
if (!order.customer) throw new InvalidOrderError('No customer');
}
function calculatePricing(items: OrderItem[]): Pricing {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * TAX_RATE;
const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}
Rule 8: Do One Thing (Single Responsibility)
A function should do one thing, do it well, and do it only.
Bad:
function emailClients(clients: Client[]): void {
for (const client of clients) {
const record = database.lookup(client.id);
if (record.isActive()) {
email(client.email, 'Welcome!');
}
}
}
Good:
function emailActiveClients(clients: Client[]): void {
const activeClients = getActiveClients(clients);
activeClients.forEach(client => sendWelcomeEmail(client));
}
function getActiveClients(clients: Client[]): Client[] {
return clients.filter(client => isClientActive(client.id));
}
function isClientActive(clientId: number): boolean {
return database.lookup(clientId).isActive();
}
function sendWelcomeEmail(client: Client): void {
email(client.email, 'Welcome!');
}
Rule 9: Keep Parameters Few (Ideally 2 or Fewer)
When you have 3 or more parameters, wrap them in an object.
Bad:
function createUser(
name: string, email: string, age: number,
role: string, department: string, isActive: boolean
): User { /* ... */ }
createUser('Alice', 'alice@example.com', 30, 'admin', 'engineering', true);
Good:
interface CreateUserParams {
name: string;
email: string;
age: number;
role: UserRole;
department: string;
isActive: boolean;
}
function createUser(params: CreateUserParams): User { /* ... */ }
createUser({
name: 'Alice',
email: 'alice@example.com',
age: 30,
role: UserRole.ADMIN,
department: 'engineering',
isActive: true,
});
Rule 10: Avoid Side Effects
Functions should not do unexpected things.
Bad:
let currentUser: User | null = null;
function checkPassword(userName: string, password: string): boolean {
const user = database.findUser(userName);
if (user && user.password === hash(password)) {
// Side effect! A password check function initializes a session
currentUser = user;
initializeSession(user);
return true;
}
return false;
}
Good:
function verifyPassword(userName: string, password: string): boolean {
const user = database.findUser(userName);
return user !== null && user.password === hash(password);
}
function login(userName: string, password: string): LoginResult {
if (!verifyPassword(userName, password)) {
return { success: false };
}
const user = database.findUser(userName);
const session = initializeSession(user);
return { success: true, user, session };
}
Rule 11: Command-Query Separation
A function should either "do something" or "return something," not both.
Bad:
function setAndCheckAttribute(name: string, value: string): boolean {
const attribute = findAttribute(name);
if (attribute) {
attribute.value = value;
return true;
}
return false;
}
// Confusing call site
if (setAndCheckAttribute('username', 'alice')) { /* ??? */ }
Good:
function attributeExists(name: string): boolean {
return findAttribute(name) !== null;
}
function setAttribute(name: string, value: string): void {
const attribute = findAttribute(name);
if (!attribute) throw new AttributeNotFoundError(name);
attribute.value = value;
}
// Clear call site
if (attributeExists('username')) {
setAttribute('username', 'alice');
}
Rule 12: DRY (Don't Repeat Yourself)
Duplicate code is a breeding ground for bugs.
Bad:
function getAdminUsers(): User[] {
const users = database.query('SELECT * FROM users');
return users
.filter(u => u.role === 'admin')
.map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
function getModeratorUsers(): User[] {
const users = database.query('SELECT * FROM users');
return users
.filter(u => u.role === 'moderator')
.map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
Good:
function getUsersByRole(role: UserRole): User[] {
const users = database.query('SELECT * FROM users WHERE role = ?', [role]);
return users.map(addDisplayName).sort(byDisplayName);
}
function addDisplayName(user: User): User {
return { ...user, displayName: `${user.firstName} ${user.lastName}` };
}
function byDisplayName(a: User, b: User): number {
return a.displayName.localeCompare(b.displayName);
}
Category 3: Comments — 4 Rules
Rule 13: Explain "Why," Not "What"
Let the code express "what" and use comments to explain "why you did it this way."
Bad:
// Get the user name
const userName = user.getName();
// Iterate over the array
for (const item of items) {
// Process the item
processItem(item);
}
Good:
// External API limits batch requests to 100 items max (API docs: https://...)
const MAX_BATCH_SIZE = 100;
// Must encode as ISO-8859-1 for legacy system compatibility
// Will switch to UTF-8 after new system migration (JIRA-4521)
const encodedData = encode(data, 'ISO-8859-1');
// Lock must be acquired first due to concurrency issue
// Details: https://wiki.internal/concurrency-fix-2024
await acquireLock(resourceId);
Rule 14: Avoid Noise Comments
Comments that add no information make code harder to read.
Bad:
/**
* Constructor
*/
constructor() {}
/**
* Returns the id
* @returns the id
*/
getId(): number {
return this.id;
}
Good:
// Code so clear it needs no comments
class User {
constructor(
private readonly id: number,
private readonly name: string,
) {}
getId(): number {
return this.id;
}
}
Rule 15: TODOs Should Follow a Standard Format
TODOs are useful tools for tracking technical debt.
Bad:
// TODO fix later
// FIXME this doesn't work
// HACK temporary workaround
Good:
// TODO(team-auth): Add OAuth2 token refresh logic [JIRA-1234] @2025-Q2
// FIXME(alice): Race condition on concurrent requests. Need distributed lock [JIRA-5678]
// HACK: Safari 17.2 flexbox bug workaround. Remove after Safari 18 fix
Rule 16: Use JSDoc/Docstrings for Public APIs
TypeScript (JSDoc):
/**
* Calculates user activity statistics for a given time period.
*
* @param userId - The user ID to query statistics for
* @param startDate - Start date (inclusive)
* @param endDate - End date (inclusive)
* @returns Activity statistics object
* @throws {UserNotFoundError} When the user does not exist
* @throws {InvalidDateRangeError} When start date is after end date
*
* @example
* const stats = await getUserActivityStats('user-123', '2025-01-01', '2025-03-31');
* console.log(stats.totalLogins); // 45
*/
async function getUserActivityStats(
userId: string,
startDate: string,
endDate: string,
): Promise<ActivityStats> {
// ...
}
Python (Docstring):
def get_user_activity_stats(
user_id: str,
start_date: str,
end_date: str,
) -> ActivityStats:
"""Calculate user activity statistics for a given time period.
Args:
user_id: The user ID to query statistics for
start_date: Start date (inclusive, ISO 8601 format)
end_date: End date (inclusive, ISO 8601 format)
Returns:
ActivityStats: Activity statistics object
Raises:
UserNotFoundError: When the user does not exist
InvalidDateRangeError: When start date is after end date
Example:
>>> stats = get_user_activity_stats('user-123', '2025-01-01', '2025-03-31')
>>> stats.total_logins
45
"""
...
Category 4: Error Handling — 4 Rules
Rule 17: Use Exceptions Instead of Error Codes
Error codes force callers to handle errors immediately, making code convoluted.
Bad:
function withdraw(account: Account, amount: number): number {
if (amount > account.balance) return -1; // Insufficient funds
if (amount < 0) return -2; // Negative amount
if (account.isFrozen) return -3; // Frozen account
account.balance -= amount;
return 0; // Success
}
const result = withdraw(myAccount, 100);
if (result === -1) { /* handle */ }
else if (result === -2) { /* handle */ }
else if (result === -3) { /* handle */ }
Good:
class InsufficientFundsError extends Error {
constructor(public readonly balance: number, public readonly amount: number) {
super(`Insufficient funds: balance ${balance}, requested ${amount}`);
this.name = 'InsufficientFundsError';
}
}
class AccountFrozenError extends Error {
constructor(public readonly accountId: string) {
super(`Account ${accountId} is frozen`);
this.name = 'AccountFrozenError';
}
}
function withdraw(account: Account, amount: number): void {
if (amount < 0) throw new InvalidAmountError(amount);
if (account.isFrozen) throw new AccountFrozenError(account.id);
if (amount > account.balance) throw new InsufficientFundsError(account.balance, amount);
account.balance -= amount;
}
Rule 18: Fail Fast
Detect invalid states as early as possible and report immediately.
Bad:
function processPayment(orderId: string, amount: number): PaymentResult {
// ... 100 lines of logic ...
// Validation only at the very end
if (!orderId) return { success: false, error: 'Invalid order ID' };
if (amount < 0) return { success: false, error: 'Invalid amount' };
// ... actual payment processing ...
}
Good:
function processPayment(orderId: string, amount: number): PaymentResult {
// Guard clauses: validate first
if (!orderId) throw new InvalidArgumentError('orderId is required');
if (amount <= 0) throw new InvalidArgumentError('amount must be positive');
const order = orderRepository.findById(orderId);
if (!order) throw new OrderNotFoundError(orderId);
return paymentGateway.charge(order, amount);
}
Rule 19: Define Custom Errors
Build an error hierarchy that matches your business logic.
abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly isOperational: boolean;
constructor(message: string, public readonly cause?: Error) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
readonly statusCode = 404;
readonly isOperational = true;
}
class ValidationError extends AppError {
readonly statusCode = 400;
readonly isOperational = true;
constructor(message: string, public readonly field: string, public readonly value: unknown) {
super(message);
}
}
class DatabaseError extends AppError {
readonly statusCode = 500;
readonly isOperational = false; // System error — alert ops
}
Rule 20: Never Catch Generic Exceptions
catch (error) or except Exception hides unexpected errors.
Bad:
try {
const data = await fetchUserData(userId);
await processData(data);
await saveToDatabase(data);
} catch (error) {
console.log('Something went wrong'); // Swallows everything
}
Good:
try {
const data = await fetchUserData(userId);
await processData(data);
await saveToDatabase(data);
} catch (error) {
if (error instanceof NetworkError) {
logger.warn('Network error fetching user data', { userId, error });
await retryWithBackoff(() => fetchUserData(userId));
} else if (error instanceof ValidationError) {
logger.error('Invalid user data', { userId, field: error.field });
throw error;
} else if (error instanceof DatabaseError) {
logger.fatal('Database error', { error });
alertOpsTeam(error);
throw error;
} else {
throw error; // Re-throw unexpected errors
}
}
Category 5: Code Organization — 5 Rules
Rule 21: Respect Vertical Formatting
Related code stays close; unrelated code is separated by blank lines.
Bad:
import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';
const MAX_RETRIES = 3;
const logger = new Logger('UserService');
export class UserService {
private db: Database;
private repo: UserRepository;
constructor(db: Database) {
this.db = db;
this.repo = new UserRepository(db);
}
async getUser(id: string): Promise<User> {
const user = await this.repo.findById(id);
if (!user) throw new NotFoundError('User not found');
return user;
}
}
Good:
import { Logger } from './logger';
import { Database } from './database';
import { UserRepository } from './repositories/user';
const MAX_RETRIES = 3;
const logger = new Logger('UserService');
export class UserService {
private db: Database;
private repo: UserRepository;
constructor(db: Database) {
this.db = db;
this.repo = new UserRepository(db);
}
async getUser(id: string): Promise<User> {
const user = await this.repo.findById(id);
if (!user) throw new NotFoundError('User not found');
return user;
}
}
Rule 22: The Newspaper Metaphor
When reading a file top to bottom, abstraction level should decrease progressively.
// 1. First: Public interface (headline)
export class OrderProcessor {
async processOrder(orderId: string): Promise<OrderResult> {
const order = await this.validateAndFetchOrder(orderId);
const pricing = this.calculatePricing(order);
return this.executePayment(order, pricing);
}
// 2. Middle: Business logic (body)
private async validateAndFetchOrder(orderId: string): Promise<Order> {
const order = await this.orderRepo.findById(orderId);
if (!order) throw new OrderNotFoundError(orderId);
this.validateOrderState(order);
return order;
}
private calculatePricing(order: Order): Pricing {
const subtotal = this.sumItems(order.items);
const discount = this.applyDiscounts(subtotal, order.coupons);
const tax = this.calculateTax(subtotal - discount);
return { subtotal, discount, tax, total: subtotal - discount + tax };
}
// 3. Last: Implementation details
private validateOrderState(order: Order): void { /* ... */ }
private sumItems(items: OrderItem[]): number { /* ... */ }
private applyDiscounts(amount: number, coupons: Coupon[]): number { /* ... */ }
private calculateTax(amount: number): number { /* ... */ }
}
Rule 23: Organize Imports
Import statements should follow a consistent ordering convention.
// 1. Node.js built-in modules
import { readFile } from 'fs/promises';
import path from 'path';
// 2. External libraries (node_modules)
import express from 'express';
import { z } from 'zod';
// 3. Internal modules — absolute paths
import { AppConfig } from '@/config';
import { Logger } from '@/utils/logger';
// 4. Internal modules — relative paths
import { UserService } from './services/user';
import { OrderService } from './services/order';
// 5. Type imports (TypeScript)
import type { User, Order } from './types';
Rule 24: Limit File Size
| Metric | Recommended | Warning | Danger |
|---|---|---|---|
| File lines | Under 200 | 200-400 | Over 400 |
| Function lines | Under 20 | 20-50 | Over 50 |
| Class methods | Under 10 | 10-20 | Over 20 |
| Parameters | 2 or fewer | 3-4 | 5 or more |
| Nesting depth | 2 levels max | 3 levels | 4+ levels |
Rule 25: Define Clear Module Boundaries
src/
├── modules/
│ ├── user/
│ │ ├── index.ts # Only export public API
│ │ ├── user.service.ts
│ │ ├── user.repository.ts
│ │ ├── user.controller.ts
│ │ ├── user.types.ts
│ │ └── __tests__/
│ ├── order/
│ │ ├── index.ts
│ │ └── ...
│ └── payment/
│ ├── index.ts
│ └── ...
├── shared/
│ ├── errors/
│ ├── utils/
│ └── types/
└── infrastructure/
├── database/
├── cache/
└── messaging/
Key principle: Modules expose only through index.ts. Internal implementation stays hidden.
// modules/user/index.ts — only the public API
export { UserService } from './user.service';
export type { User, CreateUserDTO } from './user.types';
// UserRepository is NOT exported — internal implementation
Category 6: Testing — 5 Rules
Rule 26: FIRST Principles
| Principle | Description |
|---|---|
| Fast | Tests run quickly (milliseconds) |
| Independent | No dependencies between tests |
| Repeatable | Same results in any environment |
| Self-validating | Pass/Fail is determined automatically |
| Timely | Written just before/after production code |
Rule 27: AAA Pattern (Arrange-Act-Assert)
describe('UserService', () => {
describe('createUser', () => {
it('should create user with valid data', async () => {
// Arrange
const userRepo = new InMemoryUserRepository();
const service = new UserService(userRepo);
const userData: CreateUserDTO = {
name: 'Alice',
email: 'alice@example.com',
};
// Act
const result = await service.createUser(userData);
// Assert
expect(result.name).toBe('Alice');
expect(result.email).toBe('alice@example.com');
expect(result.id).toBeDefined();
});
});
});
Rule 28: One Assert Concept per Test
Bad:
it('should handle user lifecycle', async () => {
const user = await service.createUser(data);
expect(user.id).toBeDefined();
const fetched = await service.getUser(user.id);
expect(fetched.name).toBe(data.name);
await service.updateUser(user.id, { name: 'Bob' });
const updated = await service.getUser(user.id);
expect(updated.name).toBe('Bob');
await service.deleteUser(user.id);
await expect(service.getUser(user.id)).rejects.toThrow();
});
Good:
describe('UserService', () => {
it('should create user and return with generated id', async () => {
const user = await service.createUser(validUserData);
expect(user.id).toBeDefined();
expect(user.name).toBe(validUserData.name);
});
it('should retrieve existing user by id', async () => {
const created = await service.createUser(validUserData);
const fetched = await service.getUser(created.id);
expect(fetched).toEqual(created);
});
it('should update user name', async () => {
const user = await service.createUser(validUserData);
await service.updateUser(user.id, { name: 'Bob' });
const updated = await service.getUser(user.id);
expect(updated.name).toBe('Bob');
});
it('should throw NotFoundError when getting deleted user', async () => {
const user = await service.createUser(validUserData);
await service.deleteUser(user.id);
await expect(service.getUser(user.id)).rejects.toThrow(NotFoundError);
});
});
Rule 29: Write Readable Tests
Bad:
it('test1', async () => {
const r = await s.calc({ a: 100, b: 2, c: 0.1, d: true });
expect(r).toBe(180);
});
Good:
it('should apply 10% discount when premium member orders 2 items at 100 each', async () => {
// Given
const order = createOrder({
items: [{ price: 100, quantity: 2 }],
isPremiumMember: true,
discountRate: 0.1,
});
// When
const totalPrice = await pricingService.calculateTotal(order);
// Then
expect(totalPrice).toBe(180); // (100 * 2) - 10% discount = 180
});
Rule 30: Test Names Should Describe Scenarios
Naming pattern: should [expected result] when [condition]
describe('PasswordValidator', () => {
it('should return valid when password has 8+ chars with mixed case and numbers', () => {});
it('should return invalid when password is shorter than 8 characters', () => {});
it('should return invalid when password lacks uppercase letters', () => {});
it('should throw EmptyPasswordError when password is empty string', () => {});
});
Code Smells Catalog
Representative code smells from Martin Fowler's Refactoring and their solutions:
| Code Smell | Symptom | Solution |
|---|---|---|
| Long Method | Function over 50 lines | Extract Method |
| Large Class | Class has 3+ responsibilities | Extract Class |
| Primitive Obsession | Domain expressed only with primitives | Value Object |
| Long Parameter List | 4+ parameters | Parameter Object |
| Data Clumps | Same data group repeated | Extract Class |
| Feature Envy | Excessive use of another class's data | Move Method |
| Shotgun Surgery | One change affects many classes | Move Method, Inline Class |
| Divergent Change | One class changes for multiple reasons | Extract Class |
| Comments | Excessive code-explaining comments | Rename, Extract Method |
| Dead Code | Unused code | Delete it |
| Speculative Generality | Over-abstraction for the future | Collapse Hierarchy |
| Duplicated Code | Same code in 2+ places | Extract Method |
Refactoring Techniques: Before/After
Technique 1: Guard Clauses to Remove Nesting
Before:
function getPaymentAmount(employee: Employee): number {
let result: number;
if (employee.isSeparated) {
result = separatedAmount();
} else {
if (employee.isRetired) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
return result;
}
After:
function getPaymentAmount(employee: Employee): number {
if (employee.isSeparated) return separatedAmount();
if (employee.isRetired) return retiredAmount();
return normalPayAmount();
}
Technique 2: Strategy Pattern to Eliminate Conditionals
Before:
function calculateShippingCost(order: Order): number {
if (order.shippingMethod === 'standard') {
return order.weight * 1.5;
} else if (order.shippingMethod === 'express') {
return order.weight * 3.0 + 5;
} else if (order.shippingMethod === 'overnight') {
return order.weight * 5.0 + 15;
} else if (order.shippingMethod === 'international') {
return order.weight * 10.0 + 25;
}
throw new Error('Unknown shipping method');
}
After:
interface ShippingStrategy {
calculate(weight: number): number;
}
const shippingStrategies: Record<string, ShippingStrategy> = {
standard: { calculate: (w) => w * 1.5 },
express: { calculate: (w) => w * 3.0 + 5 },
overnight: { calculate: (w) => w * 5.0 + 15 },
international: { calculate: (w) => w * 10.0 + 25 },
};
function calculateShippingCost(order: Order): number {
const strategy = shippingStrategies[order.shippingMethod];
if (!strategy) throw new UnknownShippingMethodError(order.shippingMethod);
return strategy.calculate(order.weight);
}
Technique 3: Pipeline Refactoring from Loops
Before:
function getActiveAdminEmails(users: User[]): string[] {
const result: string[] = [];
for (let i = 0; i < users.length; i++) {
if (users[i].isActive) {
if (users[i].role === 'admin') {
result.push(users[i].email.toLowerCase());
}
}
}
result.sort();
return result;
}
After:
function getActiveAdminEmails(users: User[]): string[] {
return users
.filter(user => user.isActive)
.filter(user => user.role === 'admin')
.map(user => user.email.toLowerCase())
.sort();
}
Cognitive Complexity
Cognitive complexity, as measured by SonarQube, quantifies the mental effort required to understand code.
Factors That Increase Cognitive Complexity
| Factor | Increment | Example |
|---|---|---|
| if, else if, else | +1 | Branching |
| switch case | +1 | Branching |
| for, while, do-while | +1 | Looping |
| catch | +1 | Exception handling |
| break/continue to label | +1 | Flow change |
| Nesting | +1 per level | Nested if/for |
| Logical operator chain change | +1 | a && b || c |
| Recursive call | +1 | Self-reference |
Recommended Thresholds
| Level | Cognitive Complexity | Action |
|---|---|---|
| Good | 0-5 | Maintain |
| Warning | 6-10 | Review for refactoring |
| Danger | 11-15 | Must refactor |
| Critical | 16+ | Refactor immediately |
SOLID Principles Quick Reference
S — Single Responsibility Principle
// Bad: One class, multiple responsibilities
class UserService {
createUser(data: CreateUserDTO) {}
sendEmail(to: string, body: string) {}
generatePDF(user: User) {}
logAction(action: string) {}
}
// Good: One class, one responsibility
class UserService { createUser(data: CreateUserDTO) {} }
class EmailService { send(to: string, body: string) {} }
class PDFGenerator { generate(user: User) {} }
class AuditLogger { log(action: string) {} }
O — Open/Closed Principle
interface PaymentProcessor {
process(amount: number): Promise<PaymentResult>;
}
class StripeProcessor implements PaymentProcessor {
async process(amount: number) { /* Stripe API */ }
}
class PayPalProcessor implements PaymentProcessor {
async process(amount: number) { /* PayPal API */ }
}
// Adding a new payment method requires no changes to existing code
class CryptoProcessor implements PaymentProcessor {
async process(amount: number) { /* Crypto API */ }
}
L — Liskov Substitution Principle
// Bad: Penguin cannot fly, so it shouldn't extend Bird
class Bird { fly(): void { /* flies */ } }
class Penguin extends Bird {
fly(): void { throw new Error('Cannot fly!'); }
}
// Good: Separate interfaces
interface FlyingBird { fly(): void; }
interface SwimmingBird { swim(): void; }
class Eagle implements FlyingBird { fly() {} }
class Penguin implements SwimmingBird { swim() {} }
I — Interface Segregation Principle
// Bad: Fat interface
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
}
// Good: Segregated interfaces
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Meetable { attendMeeting(): void; }
class Developer implements Workable, Feedable, Meetable {
work() {} eat() {} attendMeeting() {}
}
class Robot implements Workable {
work() {}
// No need for eat() or attendMeeting()
}
D — Dependency Inversion Principle
// Bad: High-level module depends on low-level module
class OrderService {
private mysqlDb = new MySQLDatabase();
save(order: Order) { this.mysqlDb.insert('orders', order); }
}
// Good: Depend on abstractions
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
class OrderService {
constructor(private readonly repo: OrderRepository) {}
async save(order: Order) { await this.repo.save(order); }
}
class MySQLOrderRepository implements OrderRepository { /* ... */ }
class MongoOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* ... */ }
Code Review Checklist
## Naming
- [ ] Do names reveal intent?
- [ ] No abbreviations?
- [ ] Follows team naming conventions?
## Functions
- [ ] Does each function do one thing?
- [ ] 3 or fewer parameters?
- [ ] No side effects?
## Error Handling
- [ ] Uses appropriate custom errors?
- [ ] Errors are not swallowed?
- [ ] Guard clauses for fail-fast?
## Tests
- [ ] Tests exist for new features?
- [ ] Edge cases covered?
- [ ] Test names describe scenarios?
## Code Structure
- [ ] File under 400 lines?
- [ ] Nesting depth 3 or fewer?
- [ ] DRY principle followed?
## Security
- [ ] User input validated?
- [ ] No SQL injection or XSS risks?
- [ ] No sensitive info in logs?
Practical Quiz
Quiz 1: What are the problems with this code?
function proc(lst: any[], flg: boolean): any[] {
let res: any[] = [];
for (let i = 0; i < lst.length; i++) {
if (flg) {
if (lst[i].a > 0) {
res.push(lst[i].a * 2);
}
} else {
if (lst[i].b !== null) {
res.push(lst[i].b);
}
}
}
return res;
}
Problems:
- Naming: proc, lst, flg, res, a, b are all meaningless
- Types:
anyoveruse - Responsibility: A flag parameter signals two functions are needed
- Nesting depth: 3 levels deep
- Imperative loop: Can be replaced with filter/map pipeline
Refactored:
function getDoubledPositiveAmounts(transactions: Transaction[]): number[] {
return transactions
.filter(t => t.amount > 0)
.map(t => t.amount * 2);
}
function getNonNullBalances(accounts: Account[]): number[] {
return accounts
.filter(a => a.balance !== null)
.map(a => a.balance);
}
Quiz 2: Improve this error handling code.
async function getData(id: string) {
try {
const res = await fetch(`/api/data/${id}`);
const data = await res.json();
return data;
} catch (e) {
console.log(e);
return null;
}
}
Problems:
- Swallows all errors
- Poor logging with
console.log - Does not check HTTP status codes
- Return type is ambiguous (nullable)
Refactored:
class ApiError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly endpoint: string,
) {
super(message);
this.name = 'ApiError';
}
}
async function fetchDataById(id: string): Promise<UserData> {
const endpoint = `/api/data/${id}`;
const response = await fetch(endpoint);
if (!response.ok) {
throw new ApiError(
`Failed to fetch data: ${response.statusText}`,
response.status,
endpoint,
);
}
return response.json() as Promise<UserData>;
}
Quiz 3: Find the SOLID violations in this class.
class ReportGenerator {
generateReport(data: any[]): string {
if (!data.length) throw new Error('No data');
let html = '<table>';
for (const row of data) {
html += `<tr><td>${row.name}</td><td>${row.value}</td></tr>`;
}
html += '</table>';
fs.writeFileSync('/tmp/report.html', html);
sendEmail('admin@company.com', 'Report', html);
return html;
}
}
Violations:
- SRP: Validation, HTML generation, file saving, email sending — 4 responsibilities
- OCP: Adding PDF format requires class modification
- DIP: Direct dependency on fs and sendEmail
Refactored:
interface ReportFormatter { format(data: ReportData[]): string; }
interface ReportExporter { export(content: string): Promise<void>; }
class HtmlFormatter implements ReportFormatter {
format(data: ReportData[]): string { /* ... */ }
}
class ReportService {
constructor(
private formatter: ReportFormatter,
private exporters: ReportExporter[],
) {}
async generate(data: ReportData[]): Promise<string> {
if (!data.length) throw new EmptyDataError();
const content = this.formatter.format(data);
await Promise.all(this.exporters.map(e => e.export(content)));
return content;
}
}
Quiz 4: Calculate the cognitive complexity of this function.
function calculatePrice(product: Product, user: User): number {
let price = product.basePrice;
if (user.isPremium) { // +1
if (product.category === 'electronics') { // +2 (nesting)
price *= 0.8;
} else { // +1
price *= 0.9;
}
} else { // +1
if (user.hasCoupon) { // +2 (nesting)
price *= 0.95;
}
}
if (product.isOnSale && user.isPremium) { // +1
price *= 0.9;
}
return price;
}
Cognitive Complexity: 8 (warning level, consider refactoring)
Refactored:
function calculatePrice(product: Product, user: User): number {
const basePrice = product.basePrice;
const memberDiscount = getMemberDiscount(user, product.category);
const couponDiscount = getCouponDiscount(user);
const saleDiscount = getSaleDiscount(product, user);
return basePrice * memberDiscount * couponDiscount * saleDiscount;
}
function getMemberDiscount(user: User, category: string): number {
if (!user.isPremium) return 1;
return category === 'electronics' ? 0.8 : 0.9;
}
function getCouponDiscount(user: User): number {
return !user.isPremium && user.hasCoupon ? 0.95 : 1;
}
function getSaleDiscount(product: Product, user: User): number {
return product.isOnSale && user.isPremium ? 0.9 : 1;
}
Quiz 5: Improve this test code.
test('test1', () => {
const s = new CalcService();
expect(s.add(1, 2)).toBe(3);
expect(s.add(-1, 1)).toBe(0);
expect(s.subtract(5, 3)).toBe(2);
expect(s.multiply(3, 4)).toBe(12);
expect(s.divide(10, 2)).toBe(5);
expect(() => s.divide(1, 0)).toThrow();
});
Refactored:
describe('CalcService', () => {
const calculator = new CalcService();
describe('add', () => {
it('should return sum of two positive numbers', () => {
expect(calculator.add(1, 2)).toBe(3);
});
it('should handle negative numbers', () => {
expect(calculator.add(-1, 1)).toBe(0);
});
});
describe('divide', () => {
it('should return quotient of two numbers', () => {
expect(calculator.divide(10, 2)).toBe(5);
});
it('should throw DivisionByZeroError when divisor is zero', () => {
expect(() => calculator.divide(1, 0)).toThrow(DivisionByZeroError);
});
});
});
Tools and Automation
ESLint + Prettier Configuration
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"max-lines": ["warn", { "max": 400 }],
"max-lines-per-function": ["warn", { "max": 50 }],
"max-params": ["warn", { "max": 3 }],
"max-depth": ["warn", { "max": 3 }],
"complexity": ["warn", { "max": 10 }],
"no-else-return": "error",
"prefer-const": "error",
"no-var": "error"
}
}
SonarQube Quality Gate
sonar.qualitygate.conditions:
- metric: cognitive_complexity
operator: GT
error: 15
- metric: duplicated_lines_density
operator: GT
error: 3
- metric: coverage
operator: LT
error: 80
References
- Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship" (2008)
- Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018, 2nd Edition)
- G. Ann Campbell, "Cognitive Complexity: A new way of measuring understandability" — SonarSource (2023)
- Google Engineering Practices — Code Review Guidelines
- Airbnb JavaScript Style Guide
- TypeScript Deep Dive — Style Guide
- PEP 8 — Python Style Guide
- "The Art of Readable Code" by Dustin Boswell and Trevor Foucher (2011)
- SonarQube Documentation — Cognitive Complexity
- ESLint Rules Reference
- Kent Beck, "Test Driven Development: By Example" (2002)
- Michael Feathers, "Working Effectively with Legacy Code" (2004)
- "A Philosophy of Software Design" by John Ousterhout (2018)
- Clean Code JavaScript — GitHub (ryanmcdermott/clean-code-javascript)