Welcome to this course. The focus will be "Using Sprout and Wrap Techniques for Refactoring and to Expand Capabilities"! To start off, we will explore the sprout method, a technique that can, among other things, help us safely add new features to existing codebases.
This method is particularly useful when working with established codebases, where the risk of breaking existing functionality is high. By the end of this lesson, we'll understand how to use the sprout method to enhance our code's modularity and testability.
The sprout method is a refactoring technique that involves creating new methods or classes to encapsulate additional functionality. This approach allows us to introduce new features without altering existing code, thereby minimizing the risk of breaking it.
By isolating new behavior in separate methods, we can improve the modularity of our code, making it easier to understand and maintain. The sprout method is particularly effective in scenarios where we need to expand capabilities while preserving the original behavior.
Let's walk through a step-by-step guide on how to apply the sprout method to add new features. We'll use a simple example to illustrate the process:
Scala1import model.{Order, OrderItem} 2import java.time.LocalDateTime 3 4class OrderProcessor: 5 def processOrder(order: Order): (Boolean, Order) = 6 val totalAmount = order.items.map(item => item.price * item.quantity).sum 7 8 (true, order.copy( 9 processedAt = Some(LocalDateTime.now), 10 orderTotal = totalAmount 11 ))
Suppose we want to add a surcharge to the order total. Instead of modifying the existing processOrder
method, we can create a new method called processOrderWithSurcharge
:
Scala1 def processOrderWithSurcharge(order: Order, surchargePercentage: BigDecimal): (Boolean, Order) = 2 if surchargePercentage < 0 then 3 throw IllegalArgumentException("Surcharge percentage cannot be negative") 4 5 val (_, processedOrder) = processOrder(order) 6 val surchargeAmount = processedOrder.orderTotal * (surchargePercentage / 100) 7 8 (true, processedOrder.copy( 9 processedAt = Some(LocalDateTime.now), 10 orderTotal = processedOrder.orderTotal + surchargeAmount, 11 surchargeAmount = surchargeAmount 12 ))
By creating a new method, we encapsulate the additional functionality without altering the existing processOrder
method. This approach reduces the risk of introducing bugs into the original code.
By isolating new features in separate methods, we can write targeted tests that focus solely on the new functionality.
Here's an example of how we might test the new surcharge functionality using ScalaTest:
Scala1class OrderProcessorSpec extends AnyFunSpec: 2 describe("OrderProcessor"): 3 val processor = OrderProcessor() 4 5 it("should calculate total with surcharge correctly"): 6 // Arrange 7 val order = Order( 8 items = List( 9 OrderItem(price = BigDecimal(100.00), quantity = 1) 10 ) 11 ) 12 val surchargePercentage = BigDecimal(10) 13 14 // Act 15 val (success, processedOrder) = processor.processOrderWithSurcharge(order, surchargePercentage) 16 17 // Assert 18 assert(success) 19 assert(processedOrder.orderTotal == BigDecimal(110.00)) 20 assert(processedOrder.surchargeAmount == BigDecimal(10.00)) 21 assert(processedOrder.processedAt.isDefined)
This test verifies that the new surcharge functionality correctly calculates both the total amount and the surcharge, demonstrating how we can independently test sprouted functionality.
In this lesson, we've explored the sprout method, a powerful technique for safely adding new features to existing codebases. By creating new methods to encapsulate additional functionality, we can minimize the risk of breaking existing code and improve modularity. We've also seen how to test new functionality independently to ensure its correctness.
As we move on to the practice exercises, we'll have the opportunity to apply these concepts and reinforce our understanding of the sprout method. Good luck, and enjoy the journey of enhancing your coding skills!
