`n

Code Smell Identification - Complete Guide

Published: September 25, 2024 | Reading time: 26 minutes

Code Smells Overview

Code smells are indicators of potential problems in code quality:

Code Smell Categories
# Code Smell Categories
- Bloaters: Long methods, large classes
- Abusers: Switch statements, refused bequest
- Dispensables: Dead code, duplicate code
- Couplers: Feature envy, inappropriate intimacy
- Change Preventers: Shotgun surgery, divergent change

Bloaters - Code Smells

Long Method

Long Method Code Smell
# Long Method Code Smell

# Before: Long method with multiple responsibilities
class OrderProcessor {
  processOrder(order) {
    // Validate order
    if (!order.customerId) {
      throw new Error('Customer ID is required');
    }
    if (!order.items || order.items.length === 0) {
      throw new Error('Order must have at least one item');
    }
    if (!order.shippingAddress) {
      throw new Error('Shipping address is required');
    }
    if (!order.billingAddress) {
      throw new Error('Billing address is required');
    }
    if (!order.paymentMethod) {
      throw new Error('Payment method is required');
    }
    
    // Calculate subtotal
    let subtotal = 0;
    for (const item of order.items) {
      if (!item.price || item.price <= 0) {
        throw new Error('Invalid item price');
      }
      if (!item.quantity || item.quantity <= 0) {
        throw new Error('Invalid item quantity');
      }
      subtotal += item.price * item.quantity;
    }
    
    // Apply discounts
    let discountedTotal = subtotal;
    if (order.customerId && order.customerId.startsWith('VIP')) {
      discountedTotal *= 0.9; // 10% discount for VIP customers
    }
    if (order.items.length >= 5) {
      discountedTotal *= 0.95; // 5% discount for bulk orders
    }
    if (order.customerId && order.customerId.startsWith('PREMIUM')) {
      discountedTotal *= 0.85; // 15% discount for premium customers
    }
    
    // Calculate tax
    const taxRate = 0.08;
    const tax = discountedTotal * taxRate;
    const finalTotal = discountedTotal + tax;
    
    // Create order record
    const orderRecord = {
      id: this.generateOrderId(),
      customerId: order.customerId,
      items: order.items,
      subtotal: subtotal,
      discountedTotal: discountedTotal,
      tax: tax,
      total: finalTotal,
      shippingAddress: order.shippingAddress,
      billingAddress: order.billingAddress,
      paymentMethod: order.paymentMethod,
      status: 'pending',
      createdAt: new Date()
    };
    
    // Save to database
    this.database.save('orders', orderRecord);
    
    // Send confirmation email
    const emailContent = `Order ${orderRecord.id} has been placed successfully. Total: $${finalTotal.toFixed(2)}`;
    this.emailService.send(order.customerEmail, 'Order Confirmation', emailContent);
    
    // Update inventory
    for (const item of order.items) {
      this.inventoryService.updateStock(item.productId, -item.quantity);
    }
    
    // Log the transaction
    this.logger.info(`Order ${orderRecord.id} processed successfully for customer ${order.customerId}`);
    
    // Update customer statistics
    this.customerService.updateOrderCount(order.customerId);
    this.customerService.updateTotalSpent(order.customerId, finalTotal);
    
    // Check for loyalty program eligibility
    if (finalTotal >= 100) {
      this.loyaltyService.addPoints(order.customerId, Math.floor(finalTotal / 10));
    }
    
    return orderRecord;
  }
  
  generateOrderId() {
    return 'ORD-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
  }
}

# After: Refactored with extracted methods
class OrderProcessor {
  processOrder(order) {
    this.validateOrder(order);
    const subtotal = this.calculateSubtotal(order.items);
    const discountedTotal = this.applyDiscounts(subtotal, order);
    const finalTotal = this.calculateFinalTotal(discountedTotal);
    const orderRecord = this.createOrderRecord(order, subtotal, discountedTotal, finalTotal);
    
    this.saveOrder(orderRecord);
    this.sendConfirmationEmail(order, orderRecord);
    this.updateInventory(order.items);
    this.logTransaction(orderRecord);
    this.updateCustomerStatistics(order.customerId, finalTotal);
    this.checkLoyaltyEligibility(order.customerId, finalTotal);
    
    return orderRecord;
  }
  
  validateOrder(order) {
    const requiredFields = ['customerId', 'items', 'shippingAddress', 'billingAddress', 'paymentMethod'];
    
    for (const field of requiredFields) {
      if (!order[field]) {
        throw new Error(`${field} is required`);
      }
    }
    
    if (!order.items || order.items.length === 0) {
      throw new Error('Order must have at least one item');
    }
    
    this.validateOrderItems(order.items);
  }
  
  validateOrderItems(items) {
    for (const item of items) {
      if (!item.price || item.price <= 0) {
        throw new Error('Invalid item price');
      }
      if (!item.quantity || item.quantity <= 0) {
        throw new Error('Invalid item quantity');
      }
    }
  }
  
  calculateSubtotal(items) {
    let subtotal = 0;
    for (const item of items) {
      subtotal += item.price * item.quantity;
    }
    return subtotal;
  }
  
  applyDiscounts(subtotal, order) {
    let discountedTotal = subtotal;
    
    if (order.customerId && order.customerId.startsWith('VIP')) {
      discountedTotal *= 0.9; // 10% discount for VIP customers
    }
    if (order.items.length >= 5) {
      discountedTotal *= 0.95; // 5% discount for bulk orders
    }
    if (order.customerId && order.customerId.startsWith('PREMIUM')) {
      discountedTotal *= 0.85; // 15% discount for premium customers
    }
    
    return discountedTotal;
  }
  
  calculateFinalTotal(discountedTotal) {
    const taxRate = 0.08;
    const tax = discountedTotal * taxRate;
    return discountedTotal + tax;
  }
  
  createOrderRecord(order, subtotal, discountedTotal, finalTotal) {
    const tax = finalTotal - discountedTotal;
    
    return {
      id: this.generateOrderId(),
      customerId: order.customerId,
      items: order.items,
      subtotal: subtotal,
      discountedTotal: discountedTotal,
      tax: tax,
      total: finalTotal,
      shippingAddress: order.shippingAddress,
      billingAddress: order.billingAddress,
      paymentMethod: order.paymentMethod,
      status: 'pending',
      createdAt: new Date()
    };
  }
  
  saveOrder(orderRecord) {
    this.database.save('orders', orderRecord);
  }
  
  sendConfirmationEmail(order, orderRecord) {
    const emailContent = `Order ${orderRecord.id} has been placed successfully. Total: $${orderRecord.total.toFixed(2)}`;
    this.emailService.send(order.customerEmail, 'Order Confirmation', emailContent);
  }
  
  updateInventory(items) {
    for (const item of items) {
      this.inventoryService.updateStock(item.productId, -item.quantity);
    }
  }
  
  logTransaction(orderRecord) {
    this.logger.info(`Order ${orderRecord.id} processed successfully for customer ${orderRecord.customerId}`);
  }
  
  updateCustomerStatistics(customerId, total) {
    this.customerService.updateOrderCount(customerId);
    this.customerService.updateTotalSpent(customerId, total);
  }
  
  checkLoyaltyEligibility(customerId, total) {
    if (total >= 100) {
      this.loyaltyService.addPoints(customerId, Math.floor(total / 10));
    }
  }
  
  generateOrderId() {
    return 'ORD-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
  }
}

Large Class

Large Class Code Smell
# Large Class Code Smell

# Before: Large class with multiple responsibilities
class UserManager {
  constructor() {
    this.database = new Database();
    this.emailService = new EmailService();
    this.logger = new Logger();
    this.cache = new Cache();
    this.validator = new Validator();
    this.encryptionService = new EncryptionService();
    this.fileService = new FileService();
    this.notificationService = new NotificationService();
    this.auditService = new AuditService();
  }
  
  // User CRUD operations
  createUser(userData) {
    this.validator.validateUser(userData);
    const hashedPassword = this.encryptionService.hashPassword(userData.password);
    const user = {
      ...userData,
      password: hashedPassword,
      createdAt: new Date(),
      isActive: true
    };
    const savedUser = this.database.save('users', user);
    this.cache.set(`user:${savedUser.id}`, savedUser);
    this.logger.info(`User created: ${savedUser.id}`);
    this.auditService.log('USER_CREATED', savedUser.id);
    return savedUser;
  }
  
  getUserById(id) {
    let user = this.cache.get(`user:${id}`);
    if (!user) {
      user = this.database.findById('users', id);
      if (user) {
        this.cache.set(`user:${id}`, user);
      }
    }
    return user;
  }
  
  updateUser(id, userData) {
    this.validator.validateUser(userData);
    const user = this.database.update('users', id, userData);
    this.cache.set(`user:${id}`, user);
    this.logger.info(`User updated: ${id}`);
    this.auditService.log('USER_UPDATED', id);
    return user;
  }
  
  deleteUser(id) {
    this.database.delete('users', id);
    this.cache.delete(`user:${id}`);
    this.logger.info(`User deleted: ${id}`);
    this.auditService.log('USER_DELETED', id);
  }
  
  // Authentication methods
  authenticateUser(email, password) {
    const user = this.database.findByEmail('users', email);
    if (!user) {
      throw new Error('User not found');
    }
    
    const isValidPassword = this.encryptionService.verifyPassword(password, user.password);
    if (!isValidPassword) {
      throw new Error('Invalid password');
    }
    
    const token = this.encryptionService.generateToken(user);
    this.logger.info(`User authenticated: ${user.id}`);
    this.auditService.log('USER_AUTHENTICATED', user.id);
    return { user, token };
  }
  
  resetPassword(email) {
    const user = this.database.findByEmail('users', email);
    if (!user) {
      throw new Error('User not found');
    }
    
    const resetToken = this.encryptionService.generateResetToken();
    this.database.update('users', user.id, { resetToken, resetTokenExpiry: new Date(Date.now() + 3600000) });
    
    const emailContent = `Reset your password using this token: ${resetToken}`;
    this.emailService.send(email, 'Password Reset', emailContent);
    
    this.logger.info(`Password reset initiated for user: ${user.id}`);
    this.auditService.log('PASSWORD_RESET_INITIATED', user.id);
  }
  
  // Profile management
  updateProfile(id, profileData) {
    const user = this.getUserById(id);
    if (!user) {
      throw new Error('User not found');
    }
    
    const updatedUser = { ...user, ...profileData, updatedAt: new Date() };
    this.database.update('users', id, updatedUser);
    this.cache.set(`user:${id}`, updatedUser);
    
    this.logger.info(`Profile updated for user: ${id}`);
    this.auditService.log('PROFILE_UPDATED', id);
    return updatedUser;
  }
  
  uploadAvatar(id, file) {
    const user = this.getUserById(id);
    if (!user) {
      throw new Error('User not found');
    }
    
    const avatarUrl = this.fileService.uploadFile(file, 'avatars');
    this.database.update('users', id, { avatarUrl });
    this.cache.set(`user:${id}`, { ...user, avatarUrl });
    
    this.logger.info(`Avatar uploaded for user: ${id}`);
    this.auditService.log('AVATAR_UPLOADED', id);
    return avatarUrl;
  }
  
  // Notification methods
  sendNotification(id, message) {
    const user = this.getUserById(id);
    if (!user) {
      throw new Error('User not found');
    }
    
    this.notificationService.send(user.email, message);
    this.logger.info(`Notification sent to user: ${id}`);
  }
  
  // Search and filtering
  searchUsers(query, filters = {}) {
    let users = this.database.findAll('users');
    
    if (query) {
      users = users.filter(user => 
        user.name.toLowerCase().includes(query.toLowerCase()) ||
        user.email.toLowerCase().includes(query.toLowerCase())
      );
    }
    
    if (filters.role) {
      users = users.filter(user => user.role === filters.role);
    }
    
    if (filters.isActive !== undefined) {
      users = users.filter(user => user.isActive === filters.isActive);
    }
    
    if (filters.dateRange) {
      users = users.filter(user => 
        user.createdAt >= filters.dateRange.start &&
        user.createdAt <= filters.dateRange.end
      );
    }
    
    return users;
  }
  
  // Statistics and reporting
  getUserStatistics() {
    const users = this.database.findAll('users');
    const activeUsers = users.filter(user => user.isActive);
    const inactiveUsers = users.filter(user => !user.isActive);
    
    return {
      total: users.length,
      active: activeUsers.length,
      inactive: inactiveUsers.length,
      averageAge: this.calculateAverageAge(users),
      roleDistribution: this.calculateRoleDistribution(users)
    };
  }
  
  calculateAverageAge(users) {
    const validAges = users.filter(user => user.age).map(user => user.age);
    return validAges.length > 0 ? validAges.reduce((sum, age) => sum + age, 0) / validAges.length : 0;
  }
  
  calculateRoleDistribution(users) {
    const distribution = {};
    users.forEach(user => {
      distribution[user.role] = (distribution[user.role] || 0) + 1;
    });
    return distribution;
  }
}

# After: Refactored with extracted classes
class UserManager {
  constructor() {
    this.userRepository = new UserRepository();
    this.userCache = new UserCache();
    this.userLogger = new UserLogger();
    this.userAuditor = new UserAuditor();
  }
  
  createUser(userData) {
    const user = this.userRepository.create(userData);
    this.userCache.set(user.id, user);
    this.userLogger.logUserAction(user.id, 'created');
    this.userAuditor.audit('USER_CREATED', user.id);
    return user;
  }
  
  getUserById(id) {
    let user = this.userCache.get(id);
    if (!user) {
      user = this.userRepository.findById(id);
      if (user) {
        this.userCache.set(id, user);
      }
    }
    return user;
  }
  
  updateUser(id, userData) {
    const user = this.userRepository.update(id, userData);
    this.userCache.set(id, user);
    this.userLogger.logUserAction(id, 'updated');
    this.userAuditor.audit('USER_UPDATED', id);
    return user;
  }
  
  deleteUser(id) {
    this.userRepository.delete(id);
    this.userCache.delete(id);
    this.userLogger.logUserAction(id, 'deleted');
    this.userAuditor.audit('USER_DELETED', id);
  }
  
  searchUsers(query, filters = {}) {
    return this.userRepository.search(query, filters);
  }
  
  getUserStatistics() {
    return this.userRepository.getStatistics();
  }
}

# Extracted UserRepository class
class UserRepository {
  constructor() {
    this.database = new Database();
    this.validator = new Validator();
    this.encryptionService = new EncryptionService();
  }
  
  create(userData) {
    this.validator.validateUser(userData);
    const hashedPassword = this.encryptionService.hashPassword(userData.password);
    const user = {
      ...userData,
      password: hashedPassword,
      createdAt: new Date(),
      isActive: true
    };
    return this.database.save('users', user);
  }
  
  findById(id) {
    return this.database.findById('users', id);
  }
  
  update(id, userData) {
    this.validator.validateUser(userData);
    return this.database.update('users', id, userData);
  }
  
  delete(id) {
    this.database.delete('users', id);
  }
  
  search(query, filters = {}) {
    let users = this.database.findAll('users');
    
    if (query) {
      users = users.filter(user => 
        user.name.toLowerCase().includes(query.toLowerCase()) ||
        user.email.toLowerCase().includes(query.toLowerCase())
      );
    }
    
    if (filters.role) {
      users = users.filter(user => user.role === filters.role);
    }
    
    if (filters.isActive !== undefined) {
      users = users.filter(user => user.isActive === filters.isActive);
    }
    
    if (filters.dateRange) {
      users = users.filter(user => 
        user.createdAt >= filters.dateRange.start &&
        user.createdAt <= filters.dateRange.end
      );
    }
    
    return users;
  }
  
  getStatistics() {
    const users = this.database.findAll('users');
    const activeUsers = users.filter(user => user.isActive);
    const inactiveUsers = users.filter(user => !user.isActive);
    
    return {
      total: users.length,
      active: activeUsers.length,
      inactive: inactiveUsers.length,
      averageAge: this.calculateAverageAge(users),
      roleDistribution: this.calculateRoleDistribution(users)
    };
  }
  
  calculateAverageAge(users) {
    const validAges = users.filter(user => user.age).map(user => user.age);
    return validAges.length > 0 ? validAges.reduce((sum, age) => sum + age, 0) / validAges.length : 0;
  }
  
  calculateRoleDistribution(users) {
    const distribution = {};
    users.forEach(user => {
      distribution[user.role] = (distribution[user.role] || 0) + 1;
    });
    return distribution;
  }
}

# Extracted AuthenticationService class
class AuthenticationService {
  constructor() {
    this.database = new Database();
    this.encryptionService = new EncryptionService();
    this.emailService = new EmailService();
    this.logger = new Logger();
    this.auditService = new AuditService();
  }
  
  authenticateUser(email, password) {
    const user = this.database.findByEmail('users', email);
    if (!user) {
      throw new Error('User not found');
    }
    
    const isValidPassword = this.encryptionService.verifyPassword(password, user.password);
    if (!isValidPassword) {
      throw new Error('Invalid password');
    }
    
    const token = this.encryptionService.generateToken(user);
    this.logger.info(`User authenticated: ${user.id}`);
    this.auditService.log('USER_AUTHENTICATED', user.id);
    return { user, token };
  }
  
  resetPassword(email) {
    const user = this.database.findByEmail('users', email);
    if (!user) {
      throw new Error('User not found');
    }
    
    const resetToken = this.encryptionService.generateResetToken();
    this.database.update('users', user.id, { resetToken, resetTokenExpiry: new Date(Date.now() + 3600000) });
    
    const emailContent = `Reset your password using this token: ${resetToken}`;
    this.emailService.send(email, 'Password Reset', emailContent);
    
    this.logger.info(`Password reset initiated for user: ${user.id}`);
    this.auditService.log('PASSWORD_RESET_INITIATED', user.id);
  }
}

# Extracted ProfileService class
class ProfileService {
  constructor() {
    this.database = new Database();
    this.fileService = new FileService();
    this.logger = new Logger();
    this.auditService = new AuditService();
  }
  
  updateProfile(id, profileData) {
    const user = this.database.findById('users', id);
    if (!user) {
      throw new Error('User not found');
    }
    
    const updatedUser = { ...user, ...profileData, updatedAt: new Date() };
    this.database.update('users', id, updatedUser);
    
    this.logger.info(`Profile updated for user: ${id}`);
    this.auditService.log('PROFILE_UPDATED', id);
    return updatedUser;
  }
  
  uploadAvatar(id, file) {
    const user = this.database.findById('users', id);
    if (!user) {
      throw new Error('User not found');
    }
    
    const avatarUrl = this.fileService.uploadFile(file, 'avatars');
    this.database.update('users', id, { avatarUrl });
    
    this.logger.info(`Avatar uploaded for user: ${id}`);
    this.auditService.log('AVATAR_UPLOADED', id);
    return avatarUrl;
  }
}

# Extracted NotificationService class
class UserNotificationService {
  constructor() {
    this.notificationService = new NotificationService();
    this.logger = new Logger();
  }
  
  sendNotification(id, message) {
    const user = this.database.findById('users', id);
    if (!user) {
      throw new Error('User not found');
    }
    
    this.notificationService.send(user.email, message);
    this.logger.info(`Notification sent to user: ${id}`);
  }
}

Abusers - Code Smells

Switch Statements

Switch Statement Code Smell
# Switch Statement Code Smell

# Before: Large switch statement
class PaymentProcessor {
  processPayment(payment, paymentType) {
    switch (paymentType) {
      case 'credit_card':
        if (!payment.cardNumber || !payment.expiryDate || !payment.cvv) {
          throw new Error('Credit card details are required');
        }
        
        const cardValidator = new CreditCardValidator();
        if (!cardValidator.validate(payment.cardNumber)) {
          throw new Error('Invalid credit card number');
        }
        
        const processor = new CreditCardProcessor();
        const result = processor.charge(payment.amount, payment.cardNumber, payment.expiryDate, payment.cvv);
        
        if (result.success) {
          this.logTransaction(payment, 'credit_card', result.transactionId);
          return { success: true, transactionId: result.transactionId };
        } else {
          throw new Error('Credit card payment failed: ' + result.error);
        }
        
      case 'paypal':
        if (!payment.paypalEmail) {
          throw new Error('PayPal email is required');
        }
        
        const paypalProcessor = new PayPalProcessor();
        const paypalResult = paypalProcessor.charge(payment.amount, payment.paypalEmail);
        
        if (paypalResult.success) {
          this.logTransaction(payment, 'paypal', paypalResult.transactionId);
          return { success: true, transactionId: paypalResult.transactionId };
        } else {
          throw new Error('PayPal payment failed: ' + paypalResult.error);
        }
        
      case 'bank_transfer':
        if (!payment.accountNumber || !payment.routingNumber) {
          throw new Error('Bank account details are required');
        }
        
        const bankProcessor = new BankTransferProcessor();
        const bankResult = bankProcessor.charge(payment.amount, payment.accountNumber, payment.routingNumber);
        
        if (bankResult.success) {
          this.logTransaction(payment, 'bank_transfer', bankResult.transactionId);
          return { success: true, transactionId: bankResult.transactionId };
        } else {
          throw new Error('Bank transfer failed: ' + bankResult.error);
        }
        
      case 'cryptocurrency':
        if (!payment.walletAddress) {
          throw new Error('Wallet address is required');
        }
        
        const cryptoProcessor = new CryptoProcessor();
        const cryptoResult = cryptoProcessor.charge(payment.amount, payment.walletAddress);
        
        if (cryptoResult.success) {
          this.logTransaction(payment, 'cryptocurrency', cryptoResult.transactionId);
          return { success: true, transactionId: cryptoResult.transactionId };
        } else {
          throw new Error('Cryptocurrency payment failed: ' + cryptoResult.error);
        }
        
      case 'apple_pay':
        if (!payment.applePayToken) {
          throw new Error('Apple Pay token is required');
        }
        
        const appleProcessor = new ApplePayProcessor();
        const appleResult = appleProcessor.charge(payment.amount, payment.applePayToken);
        
        if (appleResult.success) {
          this.logTransaction(payment, 'apple_pay', appleResult.transactionId);
          return { success: true, transactionId: appleResult.transactionId };
        } else {
          throw new Error('Apple Pay payment failed: ' + appleResult.error);
        }
        
      case 'google_pay':
        if (!payment.googlePayToken) {
          throw new Error('Google Pay token is required');
        }
        
        const googleProcessor = new GooglePayProcessor();
        const googleResult = googleProcessor.charge(payment.amount, payment.googlePayToken);
        
        if (googleResult.success) {
          this.logTransaction(payment, 'google_pay', googleResult.transactionId);
          return { success: true, transactionId: googleResult.transactionId };
        } else {
          throw new Error('Google Pay payment failed: ' + googleResult.error);
        }
        
      default:
        throw new Error('Unsupported payment type: ' + paymentType);
    }
  }
  
  logTransaction(payment, type, transactionId) {
    const log = {
      type: type,
      amount: payment.amount,
      transactionId: transactionId,
      timestamp: new Date()
    };
    this.logger.log(log);
  }
}

# After: Refactored with strategy pattern
class PaymentProcessor {
  constructor() {
    this.paymentHandlers = {
      'credit_card': new CreditCardPaymentHandler(),
      'paypal': new PayPalPaymentHandler(),
      'bank_transfer': new BankTransferPaymentHandler(),
      'cryptocurrency': new CryptoPaymentHandler(),
      'apple_pay': new ApplePayPaymentHandler(),
      'google_pay': new GooglePayPaymentHandler()
    };
    this.logger = new Logger();
  }
  
  processPayment(payment, paymentType) {
    const handler = this.paymentHandlers[paymentType];
    if (!handler) {
      throw new Error('Unsupported payment type: ' + paymentType);
    }
    
    const result = handler.process(payment);
    this.logTransaction(payment, paymentType, result.transactionId);
    return result;
  }
  
  logTransaction(payment, type, transactionId) {
    const log = {
      type: type,
      amount: payment.amount,
      transactionId: transactionId,
      timestamp: new Date()
    };
    this.logger.log(log);
  }
}

# Abstract PaymentHandler
class PaymentHandler {
  process(payment) {
    throw new Error('process method must be implemented');
  }
  
  validate(payment) {
    throw new Error('validate method must be implemented');
  }
}

# Credit Card Payment Handler
class CreditCardPaymentHandler extends PaymentHandler {
  process(payment) {
    this.validate(payment);
    
    const cardValidator = new CreditCardValidator();
    if (!cardValidator.validate(payment.cardNumber)) {
      throw new Error('Invalid credit card number');
    }
    
    const processor = new CreditCardProcessor();
    const result = processor.charge(payment.amount, payment.cardNumber, payment.expiryDate, payment.cvv);
    
    if (result.success) {
      return { success: true, transactionId: result.transactionId };
    } else {
      throw new Error('Credit card payment failed: ' + result.error);
    }
  }
  
  validate(payment) {
    if (!payment.cardNumber || !payment.expiryDate || !payment.cvv) {
      throw new Error('Credit card details are required');
    }
  }
}

# PayPal Payment Handler
class PayPalPaymentHandler extends PaymentHandler {
  process(payment) {
    this.validate(payment);
    
    const processor = new PayPalProcessor();
    const result = processor.charge(payment.amount, payment.paypalEmail);
    
    if (result.success) {
      return { success: true, transactionId: result.transactionId };
    } else {
      throw new Error('PayPal payment failed: ' + result.error);
    }
  }
  
  validate(payment) {
    if (!payment.paypalEmail) {
      throw new Error('PayPal email is required');
    }
  }
}

# Bank Transfer Payment Handler
class BankTransferPaymentHandler extends PaymentHandler {
  process(payment) {
    this.validate(payment);
    
    const processor = new BankTransferProcessor();
    const result = processor.charge(payment.amount, payment.accountNumber, payment.routingNumber);
    
    if (result.success) {
      return { success: true, transactionId: result.transactionId };
    } else {
      throw new Error('Bank transfer failed: ' + result.error);
    }
  }
  
  validate(payment) {
    if (!payment.accountNumber || !payment.routingNumber) {
      throw new Error('Bank account details are required');
    }
  }
}

# Cryptocurrency Payment Handler
class CryptoPaymentHandler extends PaymentHandler {
  process(payment) {
    this.validate(payment);
    
    const processor = new CryptoProcessor();
    const result = processor.charge(payment.amount, payment.walletAddress);
    
    if (result.success) {
      return { success: true, transactionId: result.transactionId };
    } else {
      throw new Error('Cryptocurrency payment failed: ' + result.error);
    }
  }
  
  validate(payment) {
    if (!payment.walletAddress) {
      throw new Error('Wallet address is required');
    }
  }
}

# Apple Pay Payment Handler
class ApplePayPaymentHandler extends PaymentHandler {
  process(payment) {
    this.validate(payment);
    
    const processor = new ApplePayProcessor();
    const result = processor.charge(payment.amount, payment.applePayToken);
    
    if (result.success) {
      return { success: true, transactionId: result.transactionId };
    } else {
      throw new Error('Apple Pay payment failed: ' + result.error);
    }
  }
  
  validate(payment) {
    if (!payment.applePayToken) {
      throw new Error('Apple Pay token is required');
    }
  }
}

# Google Pay Payment Handler
class GooglePayPaymentHandler extends PaymentHandler {
  process(payment) {
    this.validate(payment);
    
    const processor = new GooglePayProcessor();
    const result = processor.charge(payment.amount, payment.googlePayToken);
    
    if (result.success) {
      return { success: true, transactionId: result.transactionId };
    } else {
      throw new Error('Google Pay payment failed: ' + result.error);
    }
  }
  
  validate(payment) {
    if (!payment.googlePayToken) {
      throw new Error('Google Pay token is required');
    }
  }
}

Dispensables - Code Smells

Duplicate Code

Duplicate Code Smell
# Duplicate Code Smell

# Before: Duplicate code in multiple methods
class UserService {
  createUser(userData) {
    // Validation
    if (!userData.name || userData.name.trim().length === 0) {
      throw new Error('Name is required');
    }
    if (!userData.email || userData.email.trim().length === 0) {
      throw new Error('Email is required');
    }
    if (!userData.password || userData.password.trim().length === 0) {
      throw new Error('Password is required');
    }
    if (userData.password.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    if (!this.isValidEmail(userData.email)) {
      throw new Error('Invalid email format');
    }
    
    // Create user
    const user = {
      id: this.generateUserId(),
      name: userData.name,
      email: userData.email,
      password: this.hashPassword(userData.password),
      createdAt: new Date(),
      isActive: true
    };
    
    // Save to database
    this.database.save('users', user);
    
    // Send welcome email
    const emailContent = `Welcome ${userData.name}! Your account has been created successfully.`;
    this.emailService.send(userData.email, 'Welcome', emailContent);
    
    // Log the action
    this.logger.info(`User created: ${user.id}`);
    
    return user;
  }
  
  updateUser(userId, userData) {
    // Validation
    if (userData.name && userData.name.trim().length === 0) {
      throw new Error('Name cannot be empty');
    }
    if (userData.email && userData.email.trim().length === 0) {
      throw new Error('Email cannot be empty');
    }
    if (userData.password && userData.password.trim().length === 0) {
      throw new Error('Password cannot be empty');
    }
    if (userData.password && userData.password.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    if (userData.email && !this.isValidEmail(userData.email)) {
      throw new Error('Invalid email format');
    }
    
    // Get existing user
    const user = this.database.findById('users', userId);
    if (!user) {
      throw new Error('User not found');
    }
    
    // Update user
    const updatedUser = {
      ...user,
      ...userData,
      password: userData.password ? this.hashPassword(userData.password) : user.password,
      updatedAt: new Date()
    };
    
    // Save to database
    this.database.save('users', updatedUser);
    
    // Send update notification
    const emailContent = `Your account has been updated successfully.`;
    this.emailService.send(updatedUser.email, 'Account Updated', emailContent);
    
    // Log the action
    this.logger.info(`User updated: ${userId}`);
    
    return updatedUser;
  }
  
  createAdminUser(userData) {
    // Validation
    if (!userData.name || userData.name.trim().length === 0) {
      throw new Error('Name is required');
    }
    if (!userData.email || userData.email.trim().length === 0) {
      throw new Error('Email is required');
    }
    if (!userData.password || userData.password.trim().length === 0) {
      throw new Error('Password is required');
    }
    if (userData.password.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    if (!this.isValidEmail(userData.email)) {
      throw new Error('Invalid email format');
    }
    
    // Create admin user
    const user = {
      id: this.generateUserId(),
      name: userData.name,
      email: userData.email,
      password: this.hashPassword(userData.password),
      role: 'admin',
      createdAt: new Date(),
      isActive: true
    };
    
    // Save to database
    this.database.save('users', user);
    
    // Send welcome email
    const emailContent = `Welcome ${userData.name}! Your admin account has been created successfully.`;
    this.emailService.send(userData.email, 'Admin Account Created', emailContent);
    
    // Log the action
    this.logger.info(`Admin user created: ${user.id}`);
    
    return user;
  }
  
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  hashPassword(password) {
    // Password hashing implementation
    return password; // Simplified for example
  }
  
  generateUserId() {
    return 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  }
}

# After: Refactored with extracted methods
class UserService {
  createUser(userData) {
    this.validateUserData(userData, true);
    
    const user = this.buildUser(userData, { isActive: true });
    this.saveUser(user);
    this.sendWelcomeEmail(userData.email, userData.name, 'Welcome');
    this.logUserAction(user.id, 'created');
    
    return user;
  }
  
  updateUser(userId, userData) {
    this.validateUserData(userData, false);
    
    const user = this.getUserById(userId);
    const updatedUser = this.buildUpdatedUser(user, userData);
    this.saveUser(updatedUser);
    this.sendUpdateNotification(updatedUser.email);
    this.logUserAction(userId, 'updated');
    
    return updatedUser;
  }
  
  createAdminUser(userData) {
    this.validateUserData(userData, true);
    
    const user = this.buildUser(userData, { role: 'admin', isActive: true });
    this.saveUser(user);
    this.sendWelcomeEmail(userData.email, userData.name, 'Admin Account Created');
    this.logUserAction(user.id, 'admin_created');
    
    return user;
  }
  
  validateUserData(userData, isRequired) {
    const requiredFields = ['name', 'email', 'password'];
    
    for (const field of requiredFields) {
      if (isRequired && (!userData[field] || userData[field].trim().length === 0)) {
        throw new Error(`${field} is required`);
      }
      
      if (userData[field] && userData[field].trim().length === 0) {
        throw new Error(`${field} cannot be empty`);
      }
    }
    
    if (userData.password && userData.password.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    
    if (userData.email && !this.isValidEmail(userData.email)) {
      throw new Error('Invalid email format');
    }
  }
  
  buildUser(userData, additionalFields = {}) {
    return {
      id: this.generateUserId(),
      name: userData.name,
      email: userData.email,
      password: this.hashPassword(userData.password),
      createdAt: new Date(),
      ...additionalFields
    };
  }
  
  buildUpdatedUser(existingUser, userData) {
    return {
      ...existingUser,
      ...userData,
      password: userData.password ? this.hashPassword(userData.password) : existingUser.password,
      updatedAt: new Date()
    };
  }
  
  getUserById(userId) {
    const user = this.database.findById('users', userId);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }
  
  saveUser(user) {
    this.database.save('users', user);
  }
  
  sendWelcomeEmail(email, name, subject) {
    const emailContent = `Welcome ${name}! Your account has been created successfully.`;
    this.emailService.send(email, subject, emailContent);
  }
  
  sendUpdateNotification(email) {
    const emailContent = `Your account has been updated successfully.`;
    this.emailService.send(email, 'Account Updated', emailContent);
  }
  
  logUserAction(userId, action) {
    this.logger.info(`User ${action}: ${userId}`);
  }
  
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  hashPassword(password) {
    // Password hashing implementation
    return password; // Simplified for example
  }
  
  generateUserId() {
    return 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  }
}

Couplers - Code Smells

Feature Envy

Feature Envy Code Smell
# Feature Envy Code Smell

# Before: Class using too many methods from another class
class OrderProcessor {
  constructor() {
    this.database = new Database();
    this.emailService = new EmailService();
    this.inventoryService = new InventoryService();
    this.customerService = new CustomerService();
  }
  
  processOrder(order) {
    // Validate order
    if (!order.customerId) {
      throw new Error('Customer ID is required');
    }
    
    // Get customer details
    const customer = this.customerService.getCustomerById(order.customerId);
    if (!customer) {
      throw new Error('Customer not found');
    }
    
    // Check customer status
    if (!customer.isActive) {
      throw new Error('Customer account is inactive');
    }
    
    // Check customer credit limit
    if (customer.creditLimit && order.total > customer.creditLimit) {
      throw new Error('Order exceeds credit limit');
    }
    
    // Check customer payment history
    const paymentHistory = this.customerService.getPaymentHistory(order.customerId);
    const latePayments = paymentHistory.filter(payment => payment.isLate);
    if (latePayments.length > 3) {
      throw new Error('Customer has too many late payments');
    }
    
    // Update customer statistics
    this.customerService.incrementOrderCount(order.customerId);
    this.customerService.updateTotalSpent(order.customerId, order.total);
    this.customerService.updateLastOrderDate(order.customerId, new Date());
    
    // Check inventory for each item
    for (const item of order.items) {
      const product = this.inventoryService.getProductById(item.productId);
      if (!product) {
        throw new Error(`Product ${item.productId} not found`);
      }
      
      if (product.stock < item.quantity) {
        throw new Error(`Insufficient stock for product ${item.productId}`);
      }
      
      if (!product.isActive) {
        throw new Error(`Product ${item.productId} is not available`);
      }
      
      if (product.price !== item.price) {
        throw new Error(`Price mismatch for product ${item.productId}`);
      }
    }
    
    // Update inventory
    for (const item of order.items) {
      this.inventoryService.decreaseStock(item.productId, item.quantity);
      this.inventoryService.updateProductSales(item.productId, item.quantity);
      this.inventoryService.logInventoryMovement(item.productId, 'sale', item.quantity);
    }
    
    // Create order record
    const orderRecord = {
      id: this.generateOrderId(),
      customerId: order.customerId,
      customerName: customer.name,
      customerEmail: customer.email,
      items: order.items,
      total: order.total,
      status: 'pending',
      createdAt: new Date()
    };
    
    // Save order
    this.database.save('orders', orderRecord);
    
    // Send confirmation email
    const emailContent = this.buildOrderConfirmationEmail(orderRecord, customer);
    this.emailService.send(customer.email, 'Order Confirmation', emailContent);
    
    return orderRecord;
  }
  
  buildOrderConfirmationEmail(order, customer) {
    let content = `Dear ${customer.name},\n\n`;
    content += `Your order ${order.id} has been placed successfully.\n\n`;
    content += `Order Details:\n`;
    content += `- Order ID: ${order.id}\n`;
    content += `- Total: $${order.total}\n`;
    content += `- Items: ${order.items.length}\n\n`;
    content += `Thank you for your business!`;
    
    return content;
  }
  
  generateOrderId() {
    return 'ORD-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
  }
}

# After: Refactored with proper delegation
class OrderProcessor {
  constructor() {
    this.database = new Database();
    this.emailService = new EmailService();
    this.orderValidator = new OrderValidator();
    this.orderCreator = new OrderCreator();
    this.orderNotifier = new OrderNotifier();
  }
  
  processOrder(order) {
    this.orderValidator.validateOrder(order);
    
    const orderRecord = this.orderCreator.createOrder(order);
    this.saveOrder(orderRecord);
    this.orderNotifier.sendConfirmation(orderRecord);
    
    return orderRecord;
  }
  
  saveOrder(order) {
    this.database.save('orders', order);
  }
}

# Extracted OrderValidator class
class OrderValidator {
  constructor() {
    this.customerService = new CustomerService();
    this.inventoryService = new InventoryService();
  }
  
  validateOrder(order) {
    this.validateCustomer(order.customerId);
    this.validateInventory(order.items);
  }
  
  validateCustomer(customerId) {
    if (!customerId) {
      throw new Error('Customer ID is required');
    }
    
    const customer = this.customerService.getCustomerById(customerId);
    if (!customer) {
      throw new Error('Customer not found');
    }
    
    if (!customer.isActive) {
      throw new Error('Customer account is inactive');
    }
    
    this.customerService.validateCreditLimit(customerId, order.total);
    this.customerService.validatePaymentHistory(customerId);
  }
  
  validateInventory(items) {
    for (const item of items) {
      const product = this.inventoryService.getProductById(item.productId);
      if (!product) {
        throw new Error(`Product ${item.productId} not found`);
      }
      
      if (product.stock < item.quantity) {
        throw new Error(`Insufficient stock for product ${item.productId}`);
      }
      
      if (!product.isActive) {
        throw new Error(`Product ${item.productId} is not available`);
      }
      
      if (product.price !== item.price) {
        throw new Error(`Price mismatch for product ${item.productId}`);
      }
    }
  }
}

# Extracted OrderCreator class
class OrderCreator {
  constructor() {
    this.customerService = new CustomerService();
    this.inventoryService = new InventoryService();
  }
  
  createOrder(order) {
    const customer = this.customerService.getCustomerById(order.customerId);
    
    const orderRecord = {
      id: this.generateOrderId(),
      customerId: order.customerId,
      customerName: customer.name,
      customerEmail: customer.email,
      items: order.items,
      total: order.total,
      status: 'pending',
      createdAt: new Date()
    };
    
    this.updateCustomerStatistics(order.customerId, order.total);
    this.updateInventory(order.items);
    
    return orderRecord;
  }
  
  updateCustomerStatistics(customerId, total) {
    this.customerService.incrementOrderCount(customerId);
    this.customerService.updateTotalSpent(customerId, total);
    this.customerService.updateLastOrderDate(customerId, new Date());
  }
  
  updateInventory(items) {
    for (const item of items) {
      this.inventoryService.decreaseStock(item.productId, item.quantity);
      this.inventoryService.updateProductSales(item.productId, item.quantity);
      this.inventoryService.logInventoryMovement(item.productId, 'sale', item.quantity);
    }
  }
  
  generateOrderId() {
    return 'ORD-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
  }
}

# Extracted OrderNotifier class
class OrderNotifier {
  constructor() {
    this.emailService = new EmailService();
    this.customerService = new CustomerService();
  }
  
  sendConfirmation(order) {
    const customer = this.customerService.getCustomerById(order.customerId);
    const emailContent = this.buildOrderConfirmationEmail(order, customer);
    this.emailService.send(customer.email, 'Order Confirmation', emailContent);
  }
  
  buildOrderConfirmationEmail(order, customer) {
    let content = `Dear ${customer.name},\n\n`;
    content += `Your order ${order.id} has been placed successfully.\n\n`;
    content += `Order Details:\n`;
    content += `- Order ID: ${order.id}\n`;
    content += `- Total: $${order.total}\n`;
    content += `- Items: ${order.items.length}\n\n`;
    content += `Thank you for your business!`;
    
    return content;
  }
}

Code Smell Detection Tools

Automated Detection

Static Analysis Tools

  • ESLint - JavaScript linting
  • SonarQube - Code quality analysis
  • CodeClimate - Automated code review
  • PMD - Java code analysis
  • RuboCop - Ruby code analysis
  • Pylint - Python code analysis

Code Smell Indicators

  • High cyclomatic complexity
  • Long parameter lists
  • Deep nesting levels
  • Large class sizes
  • High coupling metrics
  • Low cohesion scores

Summary

Code smell identification involves several key areas:

  • Bloaters: Long methods, large classes that need to be broken down
  • Abusers: Switch statements, refused bequest that need refactoring
  • Dispensables: Dead code, duplicate code that should be removed
  • Couplers: Feature envy, inappropriate intimacy that need decoupling

Need More Help?

Struggling with code smell identification or need help improving code quality? Our code quality experts can help you implement effective code improvement strategies.

Get Code Quality Help