Files
2026-04-12 01:06:31 +07:00

13 KiB

Backend Code Quality

SOLID principles, design patterns, clean code practices, and refactoring strategies (2025).

SOLID Principles

Single Responsibility Principle (SRP)

Concept: Class/module should have one reason to change

Bad:

class User {
  saveToDatabase() { /* ... */ }
  sendWelcomeEmail() { /* ... */ }
  generateReport() { /* ... */ }
  validateInput() { /* ... */ }
}

Good:

class User {
  constructor(public id: string, public email: string, public name: string) {}
}

class UserRepository {
  async save(user: User) { /* ... */ }
  async findById(id: string) { /* ... */ }
}

class EmailService {
  async sendWelcomeEmail(user: User) { /* ... */ }
}

class UserValidator {
  validate(userData: any) { /* ... */ }
}

class ReportGenerator {
  generateUserReport(user: User) { /* ... */ }
}

Open/Closed Principle (OCP)

Concept: Open for extension, closed for modification

Bad:

class PaymentProcessor {
  process(amount: number, method: string) {
    if (method === 'stripe') {
      // Stripe logic
    } else if (method === 'paypal') {
      // PayPal logic
    }
    // Adding new payment method requires modifying this class
  }
}

Good (Strategy Pattern):

interface PaymentStrategy {
  process(amount: number): Promise<PaymentResult>;
}

class StripePayment implements PaymentStrategy {
  async process(amount: number) {
    // Stripe-specific logic
    return { success: true, transactionId: '...' };
  }
}

class PayPalPayment implements PaymentStrategy {
  async process(amount: number) {
    // PayPal-specific logic
    return { success: true, transactionId: '...' };
  }
}

class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}

  async process(amount: number) {
    return this.strategy.process(amount);
  }
}

// Usage
const processor = new PaymentProcessor(new StripePayment());
await processor.process(100);

Liskov Substitution Principle (LSP)

Concept: Subtypes must be substitutable for base types

Bad:

class Bird {
  fly() { /* ... */ }
}

class Penguin extends Bird {
  fly() {
    throw new Error('Penguins cannot fly!');
  }
}

// Violates LSP - Penguin breaks Bird contract

Good:

interface Bird {
  move(): void;
}

class FlyingBird implements Bird {
  move() {
    this.fly();
  }
  private fly() { /* ... */ }
}

class Penguin implements Bird {
  move() {
    this.swim();
  }
  private swim() { /* ... */ }
}

Interface Segregation Principle (ISP)

Concept: Clients shouldn't depend on interfaces they don't use

Bad:

interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class Robot implements Worker {
  work() { /* ... */ }
  eat() { throw new Error('Robots don't eat'); }
  sleep() { throw new Error('Robots don't sleep'); }
}

Good:

interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

class Human implements Workable, Eatable, Sleepable {
  work() { /* ... */ }
  eat() { /* ... */ }
  sleep() { /* ... */ }
}

class Robot implements Workable {
  work() { /* ... */ }
}

Dependency Inversion Principle (DIP)

Concept: Depend on abstractions, not concretions

Bad:

class MySQLDatabase {
  query(sql: string) { /* ... */ }
}

class UserService {
  private db = new MySQLDatabase(); // Tight coupling

  async getUser(id: string) {
    return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

Good (Dependency Injection):

interface Database {
  query(sql: string, params: any[]): Promise<any>;
}

class MySQLDatabase implements Database {
  async query(sql: string, params: any[]) { /* ... */ }
}

class PostgreSQLDatabase implements Database {
  async query(sql: string, params: any[]) { /* ... */ }
}

class UserService {
  constructor(private db: Database) {} // Injected dependency

  async getUser(id: string) {
    return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
  }
}

// Usage
const db = new PostgreSQLDatabase();
const userService = new UserService(db);

Design Patterns

Repository Pattern

Concept: Abstraction layer between business logic and data access

// Domain entity
class User {
  constructor(
    public id: string,
    public email: string,
    public name: string,
  ) {}
}

// Repository interface
interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implementation
class PostgresUserRepository implements UserRepository {
  constructor(private db: Database) {}

  async findById(id: string): Promise<User | null> {
    const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
    return row ? new User(row.id, row.email, row.name) : null;
  }

  async save(user: User): Promise<void> {
    await this.db.query(
      'INSERT INTO users (id, email, name) VALUES ($1, $2, $3)',
      [user.id, user.email, user.name]
    );
  }

  // Other methods...
}

// Service layer uses repository
class UserService {
  constructor(private userRepo: UserRepository) {}

  async getUser(id: string) {
    return this.userRepo.findById(id);
  }
}

Factory Pattern

Concept: Create objects without specifying exact class

interface Notification {
  send(message: string): Promise<void>;
}

class EmailNotification implements Notification {
  async send(message: string) {
    console.log(`Email sent: ${message}`);
  }
}

class SMSNotification implements Notification {
  async send(message: string) {
    console.log(`SMS sent: ${message}`);
  }
}

class PushNotification implements Notification {
  async send(message: string) {
    console.log(`Push notification sent: ${message}`);
  }
}

class NotificationFactory {
  static create(type: 'email' | 'sms' | 'push'): Notification {
    switch (type) {
      case 'email':
        return new EmailNotification();
      case 'sms':
        return new SMSNotification();
      case 'push':
        return new PushNotification();
      default:
        throw new Error(`Unknown notification type: ${type}`);
    }
  }
}

// Usage
const notification = NotificationFactory.create('email');
await notification.send('Hello!');

Decorator Pattern

Concept: Add behavior to objects dynamically

interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  cost() {
    return 10;
  }

  description() {
    return 'Simple coffee';
  }
}

class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost() {
    return this.coffee.cost() + 2;
  }

  description() {
    return `${this.coffee.description()}, milk`;
  }
}

class SugarDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost() {
    return this.coffee.cost() + 1;
  }

  description() {
    return `${this.coffee.description()}, sugar`;
  }
}

// Usage
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.description()); // "Simple coffee, milk, sugar"
console.log(coffee.cost()); // 13

Observer Pattern (Pub/Sub)

Concept: Notify multiple objects about state changes

interface Observer {
  update(event: any): void;
}

class EventEmitter {
  private observers: Map<string, Observer[]> = new Map();

  subscribe(event: string, observer: Observer) {
    if (!this.observers.has(event)) {
      this.observers.set(event, []);
    }
    this.observers.get(event)!.push(observer);
  }

  emit(event: string, data: any) {
    const observers = this.observers.get(event) || [];
    observers.forEach(observer => observer.update(data));
  }
}

// Observers
class EmailNotifier implements Observer {
  update(event: any) {
    console.log(`Sending email about: ${event.type}`);
  }
}

class LoggerObserver implements Observer {
  update(event: any) {
    console.log(`Logging event: ${JSON.stringify(event)}`);
  }
}

// Usage
const eventEmitter = new EventEmitter();
eventEmitter.subscribe('user.created', new EmailNotifier());
eventEmitter.subscribe('user.created', new LoggerObserver());

eventEmitter.emit('user.created', { type: 'user.created', userId: '123' });

Clean Code Practices

Meaningful Names

Bad:

function d(a: number, b: number) {
  return a * b * 0.0254;
}

Good:

function calculateAreaInMeters(widthInInches: number, heightInInches: number) {
  const INCHES_TO_METERS = 0.0254;
  return widthInInches * heightInInches * INCHES_TO_METERS;
}

Small Functions

Bad:

async function processOrder(orderId: string) {
  // 200 lines of code doing everything
  // - validate order
  // - check inventory
  // - process payment
  // - update database
  // - send notifications
  // - generate invoice
}

Good:

async function processOrder(orderId: string) {
  const order = await validateOrder(orderId);
  await checkInventory(order);
  const payment = await processPayment(order);
  await updateOrderStatus(orderId, 'paid');
  await sendConfirmationEmail(order);
  await generateInvoice(order, payment);
}

Avoid Magic Numbers

Bad:

if (user.age < 18) {
  throw new Error('Too young');
}

setTimeout(fetchData, 86400000);

Good:

const MINIMUM_AGE = 18;
if (user.age < MINIMUM_AGE) {
  throw new Error('Too young');
}

const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
setTimeout(fetchData, ONE_DAY_IN_MS);

Error Handling

Bad:

try {
  const user = await db.findUser(id);
  return user;
} catch (e) {
  console.log(e);
  return null;
}

Good:

try {
  const user = await db.findUser(id);
  if (!user) {
    throw new UserNotFoundError(id);
  }
  return user;
} catch (error) {
  logger.error('Failed to fetch user', {
    userId: id,
    error: error.message,
    stack: error.stack,
  });
  throw new DatabaseError('User fetch failed', { cause: error });
}

Don't Repeat Yourself (DRY)

Bad:

app.post('/api/users', async (req, res) => {
  if (!req.body.email || !req.body.email.includes('@')) {
    return res.status(400).json({ error: 'Invalid email' });
  }
  // ...
});

app.put('/api/users/:id', async (req, res) => {
  if (!req.body.email || !req.body.email.includes('@')) {
    return res.status(400).json({ error: 'Invalid email' });
  }
  // ...
});

Good:

function validateEmail(email: string) {
  if (!email || !email.includes('@')) {
    throw new ValidationError('Invalid email');
  }
}

app.post('/api/users', async (req, res) => {
  validateEmail(req.body.email);
  // ...
});

app.put('/api/users/:id', async (req, res) => {
  validateEmail(req.body.email);
  // ...
});

Code Refactoring Techniques

Extract Method

Before:

function renderOrder(order: Order) {
  console.log('Order Details:');
  console.log(`ID: ${order.id}`);
  console.log(`Total: $${order.total}`);

  console.log('Items:');
  order.items.forEach(item => {
    console.log(`- ${item.name}: $${item.price}`);
  });
}

After:

function renderOrder(order: Order) {
  printOrderHeader(order);
  printOrderItems(order.items);
}

function printOrderHeader(order: Order) {
  console.log('Order Details:');
  console.log(`ID: ${order.id}`);
  console.log(`Total: $${order.total}`);
}

function printOrderItems(items: OrderItem[]) {
  console.log('Items:');
  items.forEach(item => {
    console.log(`- ${item.name}: $${item.price}`);
  });
}

Replace Conditional with Polymorphism

Before:

function getShippingCost(order: Order) {
  if (order.shippingMethod === 'standard') {
    return 5;
  } else if (order.shippingMethod === 'express') {
    return 15;
  } else if (order.shippingMethod === 'overnight') {
    return 30;
  }
}

After:

interface ShippingMethod {
  getCost(): number;
}

class StandardShipping implements ShippingMethod {
  getCost() {
    return 5;
  }
}

class ExpressShipping implements ShippingMethod {
  getCost() {
    return 15;
  }
}

class OvernightShipping implements ShippingMethod {
  getCost() {
    return 30;
  }
}

Code Quality Checklist

  • SOLID principles applied
  • Functions are small (< 20 lines ideal)
  • Meaningful variable/function names
  • No magic numbers (use constants)
  • Proper error handling (no silent failures)
  • DRY (no code duplication)
  • Comments explain "why", not "what"
  • Design patterns used appropriately
  • Dependency injection for testability
  • Code is readable (readable > clever)

Resources