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