Навигация по запаху кода: выявление проблем и разработка решений в проектах

Запахи кода — это тонкие индикаторы более глубоких проблем в кодовой базе, которые, если их не контролировать, могут привести к снижению удобства сопровождения, увеличению сложности и потенциальным ошибкам.

В проектах Kotlin распознавание и устранение этих запахов кода имеет первостепенное значение для создания чистого, эффективного и удобного в сопровождении кода. В этой статье рассматриваются некоторые распространенные запахи кода, их последствия и стратегии их устранения для повышения качества проектов Kotlin.

Понимание того, что такое запахи кода. Запахи кода — это закономерности в коде, которые намекают на потенциальные проблемы проектирования или реализации. Они не обязательно относятся к ошибкам, а скорее указывают на области, которые могли бы получить выгоду от рефакторинга.

Некоторые распространенные запахи кода включают длинные функции, дублированный код, большие классы, примитивную одержимость и неуместные комментарии. Выявление этих запахов требует бдительности и глубокого понимания принципов хорошего проектирования программного обеспечения.

Длинные функции. Длинные функции, часто называемые «функциями Бога», выполняют слишком много задач в одном блоке. Это не только ухудшает читаемость, но и делает обслуживание сложной задачей. Разбиение таких функций на более мелкие и более целенаправленные блоки не только улучшает читаемость, но также повышает возможность повторного использования и удобства обслуживания.

//Incorrect Example - Long Function for Wine Order: Code Smell
fun placeWineOrder(customer: Customer, items: List<WineItem>) {
    // Validate customer
    if (customer.name.isEmpty() || customer.age < 18) {
        throw IllegalArgumentException("Invalid customer.")
    }
    
    // Calculate total price
    var totalPrice = 0.0
    for (item in items) {
        if (item.quantity <= 0) {
            throw IllegalArgumentException("Invalid item quantity.")
        }
        totalPrice += item.price * item.quantity
    }
    
    // Apply discount
    if (totalPrice > 200) {
        totalPrice *= 0.9
    }
    
    // Generate order details
    val orderDetails = StringBuilder()
    orderDetails.append("Order for ${customer.name}\n")
    for (item in items) {
        orderDetails.append("${item.quantity} x ${item.name}: $${item.price * item.quantity}\n")
    }
    orderDetails.append("Total price: $totalPrice")
    
    // Send order confirmation
    EmailService.sendEmail(customer.email, "Order Confirmation", orderDetails.toString())
    
    // Update inventory
    for (item in items) {
        Inventory.updateQuantity(item, -item.quantity)
    }
    
    // Update customer points
    customer.points += (totalPrice / 10).toInt()
    Database.updateCustomer(customer)
}

//Correct Refactor - Breaking Down the Function for Wine Order
fun placeWineOrder(customer: Customer, items: List<WineItem>) {
    validateCustomer(customer)
    val totalPrice = calculateTotalPrice(items)
    applyDiscount(totalPrice)
    val orderDetails = generateOrderDetails(customer, items, totalPrice)
    sendOrderConfirmation(customer, orderDetails)
    updateInventory(items)
    updateCustomerPoints(customer, totalPrice)
}

fun validateCustomer(customer: Customer) {
    if (customer.name.isEmpty() || customer.age < 18) {
        throw IllegalArgumentException("Invalid customer.")
    }
}

fun calculateTotalPrice(items: List<WineItem>): Double {
    return items.sumByDouble { it.price * it.quantity }
}

fun applyDiscount(totalPrice: Double) {
    if (totalPrice > 200) {
        totalPrice *= 0.9
    }
}

fun generateOrderDetails(customer: Customer, items: List<WineItem>, totalPrice: Double): String {
    val orderDetails = StringBuilder()
    orderDetails.append("Order for ${customer.name}\n")
    for (item in items) {
        orderDetails.append("${item.quantity} x ${item.name}: $${item.price * item.quantity}\n")
    }
    orderDetails.append("Total price: $totalPrice")
    return orderDetails.toString()
}

fun sendOrderConfirmation(customer: Customer, orderDetails: String) {
    EmailService.sendEmail(customer.email, "Order Confirmation", orderDetails)
}

fun updateInventory(items: List<WineItem>) {
    for (item in items) {
        Inventory.updateQuantity(item, -item.quantity)
    }
}

fun updateCustomerPoints(customer: Customer, totalPrice: Double) {
    customer.points += (totalPrice / 10).toInt()
    Database.updateCustomer(customer)
}

Дублированный код. Дублированный код — это явный признак упущенных возможностей абстракции и повторного использования кода. Эти повторяющиеся блоки кода могут привести к несогласованности и ошибкам, когда потребуются изменения. Выделение общей функциональности в отдельные функции или классы обеспечивает согласованность, снижает вероятность ошибок и упрощает обслуживание.

//Code Smell
fun calculateCircleArea(radius: Double): Double {
    return 3.14 * radius * radius
}

fun calculateRectangleArea(width: Double, height: Double): Double {
    return 3.14 * width * height // Oops, copy-paste mistake
}
//Correct Refactor
fun calculateCircleArea(radius: Double): Double {
    return 3.14 * radius * radius
}

fun calculateRectangleArea(width: Double, height: Double): Double {
    return width * height
}

Большие классы. Большие классы, которые выполняют множество обязанностей, нарушают принцип единой ответственности. Такие классы со временем становятся трудными для понимания и модификации. Разбивка их на более мелкие, целенаправленные классы, соответствующие принципу единой ответственности, приводит к получению более модульного и удобного в сопровождении кода.

//Long Class - WineManager 
class WineManager {
    fun addWine(wine: Wine) {
        // Add wine to database
    }

    fun updateWine(wine: Wine) {
        // Update wine details in database
    }

    fun calculateTotalRevenue() {
        // Calculate total revenue from all wines
    }

    fun processOrder(order: Order) {
        // Validate order, deduct quantities, update inventory, and send confirmation
    }

    fun generateSalesReport() {
        // Generate sales report with various statistics
    }

    // ... more methods related to wine management ...
}
//Refactored - Smaller, Focused Classes:
class WineCatalog {
    fun addWine(wine: Wine) {
        // Add wine to database
    }

    fun updateWine(wine: Wine) {
        // Update wine details in database
    }
}

class RevenueCalculator {
    fun calculateTotalRevenue() {
        // Calculate total revenue from all wines
    }
}

class OrderProcessor {
    fun processOrder(order: Order) {
        // Validate order, deduct quantities, update inventory, and send confirmation
    }
}

class SalesReportGenerator {
    fun generateSalesReport() {
        // Generate sales report with various statistics
    }
}

Примитивная одержимость: Примитивная одержимость предполагает использование примитивных типов данных для сложных концепций предметной области, что приводит к отсутствию ясности и повышенному риску ошибок. Создавая специальные классы для инкапсуляции концепций предметной области и их поведения (например, класс Currency), разработчики повышают ясность кода, безопасность типов и удобство сопровождения.

// Wrong: Using primitive types for currencies
val usdAmount: Double = 100.0
val eurAmount: Double = 80.0
val exchangeRateToUSD: Double = 1.18

// Performing calculations without type safety or clarity
val totalAmount: Double = usdAmount + (eurAmount * exchangeRateToUSD)
println("Total amount in USD: $totalAmount")
// Correct: Using a dedicated Currency class
class Currency(val code: String, val exchangeRateToUSD: Double) {
    fun convertToUSD(amount: Double): Double {
        return amount * exchangeRateToUSD
    }
}

fun main() {
    val usd = Currency("USD", 1.0)
    val eur = Currency("EUR", 1.18)
    val amountInEur = 80.0

    // Using the Currency class for conversions
    val amountInUSD = eur.convertToUSD(amountInEur)
    println("Amount in USD: $amountInUSD")
}

Неуместные комментарии. Неуместные или чрезмерные комментарии часто указывают на то, что код не требует пояснений. Вместо того, чтобы загромождать код комментариями, стремление к самодокументируемому коду со значимыми именами переменных и функций делает базу кода более читабельной и уменьшает потребность в комментариях, которые могут устареть.

//Code Smell - Inappropriate Comments:
class WineManager {
    // This is the WineManager class
    // It manages various wine-related functionalities
    // Constructor for creating a new WineManager instance
    constructor() {
        // Initialize the WineManager instance
    }

    // Method to add a wine to the catalog
    fun addWine(wine: Wine) {
        // Code to add the wine to the catalog
    }
    
    // Method to update the wine details
    // Takes a Wine object as parameter
    fun updateWine(wine: Wine) {
        // Code to update the wine details
    }

    // ... other methods with similar comments ...
}
//Refactored - Clearer Code, Concise Comments:
class WineManager {
    constructor() {
        // Initialize the WineManager
    }

    fun addWine(wine: Wine) {
        // Add the wine to the catalog
    }
    
    fun updateWine(wine: Wine) {
        // Update the wine details
    }

    // ... other methods without redundant comments ...
}

Длинные списки параметров. Функции со слишком большим количеством параметров могут сбивать с толку, и часто это признак того, что функция пытается сделать слишком много.

//Code Smell : Long Parameter
fun processWineOrder(
    customerId: Int,
    customerName: String,
    customerEmail: String,
    wines: List<Wine>,
    quantities: List<Int>,
    isPreferredCustomer: Boolean,
    shippingAddress: String,
    billingAddress: String
) {
    // Process the order using the provided parameters
}
//Refactored - Introducing a Data Class:
data class WineOrder(
    val customer: Customer,
    val items: List<OrderItem>,
    val shippingAddress: String,
    val billingAddress: String
)

data class Customer(
    val id: Int,
    val name: String,
    val email: String,
    val isPreferred: Boolean
)

data class OrderItem(
    val wine: Wine,
    val quantity: Int
)

fun processWineOrder(order: WineOrder) {
    // Process the order using the encapsulated details
}

Стратегии устранения запахов кода. Устранение запахов кода требует стратегического подхода:

  1. Проверки кода. Регулярные проверки кода с коллегами позволяют выявить запахи кода, которые можно не заметить.
  2. Рефакторинг. Уделяйте время рефакторингу, чтобы систематически устранять неприятные запахи кода, сохраняя при этом функциональность.
  3. Инструменты анализа кода. Используйте такие инструменты, как Detectekt, в процессе сборки, чтобы автоматически обнаруживать запахи кода.
  4. Документация и обучение. Расскажите своей команде о запахах кода, их последствиях и рекомендациях по их устранению.

Запахи кода подобны указателям, ведущим разработчиков к более удобному в сопровождении и эффективному коду. Распознавание этих запахов и понимание того, как с ними бороться, — жизненно важный навык для любого разработчика.

Активно выявляя и устраняя запахи кода, проекты можно поднять на новый уровень читаемости, удобства сопровождения и качества. В конечном счете, стремление к чистому коду — это постоянный путь, в котором бдительность и стремление к совершенствованию приводят к созданию программного обеспечения, которое не только работает, но и работает хорошо.