Welcome to the very first lesson of the "Applying Clean Code Principles in Scala" 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 deeply into issues caused by repetitive code and explore strategies to combat redundancy. 🚀
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.
To adhere to the DRY principle and avoid repeating yourself, several strategies can be employed:
-
Extracting Method: 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. Using
val
for immutability further centralizes change, reducing potential errors. -
Replace Temp with Query: Use methods to compute values on demand rather than storing them in temporary variables, aiding in readability and reducing redundancy.
Consider the following problematic code snippet, where repetitive logic is used for calculating the total price based on different shipping methods:
Scala1// Issues: 2// 1. Duplicated logic for calculating items total 3// 2. Same tax calculation repeated in both methods 4// 3. Redundant mapping of items to calculate total price 5case class Item(price: Double, quantity: Int) 6case class Order(items: Seq[Item], tax: Double) 7 8def calculateClickAndCollectTotal(order: Order): Double = 9 val itemsTotal = order.items.map(item => item.price * item.quantity).sum // Duplicated calculation 10 val shippingCost = if itemsTotal > 100 then 0 else 5 11 itemsTotal + shippingCost + order.tax 12 13def calculatePostShipmentTotal(order: Order, isExpress: Boolean): Double = 14 val itemsTotal = order.items.map(item => item.price * item.quantity).sum // Same calculation repeated 15 val shippingCost = if isExpress then itemsTotal * 0.1 else itemsTotal * 0.05 16 itemsTotal + shippingCost + order.tax
Both methods contain duplicated logic for calculating the total price of items, making them error-prone and hard to maintain. Now, let's refactor this code.
By consolidating the shared logic into a separate function, we can eliminate redundancy and streamline updates:
Scala1// Improvements: 2// 1. Extracted common calculation into a separate method 3// 2. Single source of truth for items total calculation 4// 3. Easier maintenance as changes to total calculation only needed in one place 5case class Item(price: Double, quantity: Int) 6case class Order(items: Seq[Item], tax: Double) 7 8def calculateClickAndCollectTotal(order: Order): Double = 9 val itemsTotal = calculateItemsTotal(order) // Using extracted method 10 val shippingCost = if itemsTotal > 100 then 0 else 5 11 itemsTotal + shippingCost + order.tax 12 13def calculatePostShipmentTotal(order: Order, isExpress: Boolean): Double = 14 val itemsTotal = calculateItemsTotal(order) // Using same extracted method 15 val shippingCost = if isExpress then itemsTotal * 0.1 else itemsTotal * 0.05 16 itemsTotal + shippingCost + order.tax 17 18def calculateItemsTotal(order: Order): Double = // Centralized calculation logic 19 order.items.map(item => item.price * item.quantity).sum
By extracting the calculateItemsTotal
function, we centralize the logic of item total calculation, leading to cleaner, more maintainable code.
Let's look at an example that deals with repeated calculations for discount rates:
Scala1// Issues: 2// 1. Mutable variables (var) used unnecessarily 3// 2. Multiple calculations modifying the same variable 4// 3. Discount calculations are scattered and hard to follow 5def applyDiscount(price: Double, customer: Customer): Double = 6 var loyaltyDiscount = customer.loyaltyLevel * 0.02 // Mutable variable 7 var discountedPrice = price * (1 - loyaltyDiscount) // Multiple modifications 8 9 var seasonalDiscount = 0.10 // Another mutable variable 10 discountedPrice *= (1 - seasonalDiscount) // Modifying price again 11 discountedPrice
Here, the discount rates are scattered throughout the code, which complicates management and updates. It also uses var
, which is not common in idiomatic Scala where val
usage is preferred.
We can simplify this by extracting the discount rates into immutable variables:
Scala1// Improvements: 2// 1. Immutable variables (val) used instead of var 3// 2. Single calculation for final price 4// 3. Clear and centralized discount calculations 5def applyDiscount(price: Double, customer: Customer): Double = 6 val loyaltyDiscount = customer.loyaltyLevel * 0.02 // Immutable variables 7 val seasonalDiscount = 0.10 8 9 val finalPrice = price * (1 - loyaltyDiscount) * (1 - seasonalDiscount) // Single calculation 10 finalPrice
With the totalDiscount
variable, the logic is cleaner, more readable, and allows changes in just one place. 🎉
Our final example involves temporary variables that lead to repetition:
Scala1// Issues: 2// 1. Duplicated newCustomer calculation 3// 2. Same date comparison logic repeated 4// 3. Temporary variable leading to code duplication 5def isEligibleForDiscount(customer: Customer): Boolean = 6 val newCustomer = customer.signUpDate.isAfter(LocalDate.now.minusMonths(3)) // Duplicated logic 7 newCustomer && customer.purchaseHistory.size > 5 8 9def isEligibleForLoyaltyProgram(customer: Customer): Boolean = 10 val newCustomer = customer.signUpDate.isAfter(LocalDate.now.minusMonths(3)) // Same calculation repeated 11 newCustomer || customer.loyaltyLevel > 3
The variable newCustomer
is used in multiple places, causing duplicated logic.
Let's refactor by extracting the logic into a method, reducing duplication and enhancing modularity:
Scala1// Improvements: 2// 1. Extracted newCustomer check into a dedicated method 3// 2. Single source of truth for new customer logic 4// 3. More maintainable and clearer business rules 5def isEligibleForDiscount(customer: Customer): Boolean = 6 isNewCustomer(customer) && customer.purchaseHistory.size > 5 // Using extracted method 7 8def isEligibleForLoyaltyProgram(customer: Customer): Boolean = 9 isNewCustomer(customer) || customer.loyaltyLevel > 3 // Reusing same method 10 11def isNewCustomer(customer: Customer): Boolean = // Centralized new customer logic 12 customer.signUpDate.isAfter(LocalDate.now.minusMonths(3))
By creating the isNewCustomer
method, we've simplified the code and made it more maintainable. 🚀
In this lesson, you learned about the DRY principle and strategies like Extracting Method, Extracting Variable, and Replace Temp with Query to eliminate code redundancy. These strategies help to 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! 😊