Lesson 1
Applying Clean Code Principles: Embracing DRY in Kotlin
Introduction

Welcome to the very first lesson of the "Applying Clean Code Principles" course! In this lesson, we will focus on a fundamental concept in clean coding: the DRY ("Don't Repeat Yourself") principle. Understanding DRY is crucial for writing efficient, maintainable, and clean code. This principle is not just important for coding interviews but also in everyday software development. Today, we will dive deep into issues caused by repetitive code and explore strategies to combat redundancy. 🚀

Understanding the Problem

Repetitive functionality in code can introduce several issues that affect the efficiency and maintainability of your software:

  • Code Bloat: Repeating similar code across different parts of your application unnecessarily increases the size of the codebase. This makes the code harder to navigate and increases the chances of introducing errors.

  • Risk of Inconsistencies: When similar pieces of logic are scattered across different areas, it's easy for them to become out of sync during updates or bug fixes. This can result in logic discrepancies and potentially introduce new problems.

  • Maintenance Challenges: Updating code often requires modifications in multiple places, leading to increased work and a higher likelihood of errors. Redundant code makes it difficult for developers to ensure all necessary changes have been made consistently.

DRY Strategies

To adhere to the DRY principle and avoid repeating yourself, several strategies can be employed:

  • Extracting Function: Move repeated logic into a dedicated function that can be called wherever needed. This promotes reuse and simplifies updates.

  • Extracting Variable: Consolidate repeated expressions or values into variables. This centralizes changes, reducing the potential for errors.

  • Replace Temp with Query: Use a function to compute values on demand rather than storing them in temporary variables, aiding in readability and reducing redundancy.

Extracting Function

Consider the following problematic code snippet where repetitive logic is used for calculating the total price based on different shipping methods:

Kotlin
1data class Item(val price: Double, val quantity: Int) 2data class Order(val items: List<Item>, val tax: Double) 3 4fun calculateClickAndCollectTotal(order: Order): Double { 5 var itemsTotal = 0.0 6 for (item in order.items) { 7 itemsTotal += item.price * item.quantity 8 } 9 val shippingCost = if (itemsTotal > 100) 0 else 5 10 return itemsTotal + shippingCost + order.tax 11} 12 13fun calculatePostShipmentTotal(order: Order, isExpress: Boolean): Double { 14 var itemsTotal = 0.0 15 for (item in order.items) { 16 itemsTotal += item.price * item.quantity 17 } 18 val shippingCost = if (isExpress) itemsTotal * 0.1 else itemsTotal * 0.05 19 return itemsTotal + shippingCost + order.tax 20}

Both functions contain duplicated logic for calculating the total price of items, making them error-prone and hard to maintain. Now, let's refactor this code.

Extracting Function: Refactored

By consolidating the shared logic into a separate function, we can eliminate redundancy and streamline updates:

Kotlin
1data class Item(val price: Double, val quantity: Int) 2data class Order(val items: List<Item>, val tax: Double) 3 4fun calculateClickAndCollectTotal(order: Order): Double { 5 val itemsTotal = order.calculateItemsTotal() 6 val shippingCost = if (itemsTotal > 100) 0 else 5 7 return itemsTotal + shippingCost + order.tax 8} 9 10fun calculatePostShipmentTotal(order: Order, isExpress: Boolean): Double { 11 val itemsTotal = order.calculateItemsTotal() 12 val shippingCost = if (isExpress) itemsTotal * 0.1 else itemsTotal * 0.05 13 return itemsTotal + shippingCost + order.tax 14} 15 16private fun Order.calculateItemsTotal(): Double { 17 return items.sumOf { it.price * it.quantity } 18}

By extracting the calculateItemsTotal function, we centralize the logic of item total calculation, leading to cleaner, more maintainable code.

Extracting Variable

Let's look at another example dealing with repeated calculations for discount rates:

Kotlin
1data class Customer(val loyaltyLevel: Int) 2 3fun applyDiscount(price: Double, customer: Customer): Double { 4 val loyaltyDiscount = customer.loyaltyLevel * 0.02 5 var discountedPrice = price * (1 - loyaltyDiscount) 6 // Additional discounts 7 val seasonalDiscount = 0.10 8 discountedPrice *= (1 - seasonalDiscount) 9 return discountedPrice 10}

Here, the discount rates are scattered throughout the code, complicating management and updates.

Extracting Variable: Refactored

We can simplify this by extracting the discount rates into variables:

Kotlin
1data class Customer(val loyaltyLevel: Int) 2 3fun applyDiscount(price: Double, customer: Customer): Double { 4 val loyaltyDiscount = customer.loyaltyLevel * 0.02 5 val seasonalDiscount = 0.10 6 7 val totalDiscount = 1 - loyaltyDiscount - seasonalDiscount 8 return price * totalDiscount 9}

Now, with the totalDiscount variable, the logic is cleaner, more readable, and allows changes in just one place. 🎉

Replace Temp with Query

Our final example involves temporary variables leading to repetition:

Kotlin
1data class Customer(val signUpDate: LocalDate, val purchaseHistory: List<Any>, val loyaltyLevel: Int) 2 3fun isEligibleForDiscount(customer: Customer): Boolean { 4 val newCustomer = customer.signUpDate.isAfter(LocalDate.now().minusMonths(3)) 5 return newCustomer && customer.purchaseHistory.size > 5 6} 7 8fun isEligibleForLoyaltyProgram(customer: Customer): Boolean { 9 val newCustomer = customer.signUpDate.isAfter(LocalDate.now().minusMonths(3)) 10 return newCustomer || customer.loyaltyLevel > 3 11}

The variable newCustomer is used in multiple places, causing duplicated logic.

Replace Temp with Query: Refactored

Let's refactor by extracting the logic into a method, reducing duplication and enhancing modularity:

Kotlin
1data class Customer(val signUpDate: LocalDate, val purchaseHistory: List<Any>, val loyaltyLevel: Int) 2 3fun isEligibleForDiscount(customer: Customer): Boolean { 4 return customer.isNewCustomer() && customer.purchaseHistory.size > 5 5} 6 7fun isEligibleForLoyaltyProgram(customer: Customer): Boolean { 8 return customer.isNewCustomer() || customer.loyaltyLevel > 3 9} 10 11private fun Customer.isNewCustomer(): Boolean { 12 return signUpDate.isAfter(LocalDate.now().minusMonths(3)) 13}

By creating the isNewCustomer function, we've simplified the code and made it more maintainable. 🚀

Summary and Preparation for Practice

In this lesson, you learned about the DRY principle and strategies like Extracting Function, Extracting Variable, and Replace Temp with Query to eliminate code redundancy. These strategies help create code that is easier to maintain, enhance, and understand. Next, you'll have the opportunity to apply these concepts in practical exercises, strengthening your ability to refactor code and uphold clean coding standards. Happy coding! 😊

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.