Write a failing test (red), make it pass (green), then refactor.
The Red-Green-Refactor cycle is a fundamental concept in Test-Driven Development (TDD). It's a cyclic approach to software development that aims at creating robust, error-free code. Let's break it down:
1. Red: Write a test that should fail (because the functionality it tests isn't implemented yet).
2. Green: Write the minimum amount of code to make the test pass.
3. Refactor: Refactor the code while making sure that all tests still pass.
Assume we're working on a web application where we have a shopping cart. We want to add a feature to calculate the total price of the items in the cart. The cart is represented as an array of items, where each item is an object with properties, price and quantity.
Step 1 - Red
We start by writing a failing test for our new function calculateTotal.
If we run this test now, it would fail because we haven't defined the calculateTotal function yet.
Step 2 - Green
Next, we write just enough code to make the test pass.
This function calculates the total price by iterating over each item in the cart and adding the product of the item's price and quantity to the total. If we run the test now, it should pass.
Step 3 - Refactor
This function does the same thing as before, but now in a more concise and declarative way. We should run the tests again to make sure that the refactored code still works as expected.
This cycle ensures that we write only the code necessary to meet our requirements, which are defined by our tests. After the basic requirements are met, we can then refactor the code to improve its structure and maintainability while having tests to guard against regressions. This is a simplified example, but the same cycle would be used in a real-world setting with more complex functions and multiple related tests.
When a function does more than one thing or becomes too long, it's often a good idea to extract parts of it into smaller, reusable functions.
The 'Extract Function' refactoring technique is a very common approach used to break down large or complex functions into smaller, more manageable ones. Let's look at an example from a hypothetical online store's codebase.
Imagine we have a function processOrder that processes a customer's order. The function takes an order object and does several things: it calculates the total price, checks the inventory, places the order, and sends an email confirmation to the user.
This function works, but it's doing a lot of different things, making it hard to understand, test, and maintain. Let's apply the 'Extract Function' technique to refactor it.
We can identify four distinct operations in the processOrder function, and we can extract each of them into their own function:
- Calculating the total price (calculateTotal)
- Checking the inventory (checkInventory)
- Updating the inventory (updateInventory)
- Sending the confirmation email (sendConfirmationEmail)
Now, we can simplify the `processOrder` function by using the extracted functions:
The processOrder function now is much more readable and maintainable. Each operation is isolated in its own function, which makes it easier to test and update if necessary. This is a practical real-world example of how the 'Extract Function' refactoring technique can be used to improve your code.
Replace Temp with Query
Replace temporary variables that are derived from the result of a function with a call to the function itself.
The 'Replace Temp with Query' refactoring technique is about replacing temporary variables with a method call or a query that retrieves the same data.
For instance, let's consider a scenario where we have an online shopping cart and we're calculating a discount based on the total price of the items in the cart. The original code might look something like this:
Here, basePrice is a temporary variable that stores the total price of the items in the cart. The calculateDiscount method then calculates the discount based on this value.
We can apply the 'Replace Temp with Query' technique by extracting the calculation of the basePrice into its own method, which can be called directly in the discount calculation:
In this refactored version, calculateBasePrice is a query that replaces the temporary variable basePrice. Now, whenever we need to calculate the base price, we can call the calculateBasePrice method, which makes the code cleaner and more maintainable. The calculateDiscount method has also become more readable, as it's clear where the base price comes from.
Replace Conditional with Polymorphism
Use inheritance and polymorphism to eliminate conditional statements.
'Replace Conditional with Polymorphism' is a refactoring technique that involves replacing conditional code with polymorphic calls. Let's consider a scenario where an online store provides different types of memberships to its customers, and each membership type has a different discount policy.
Here's how the code might look initially, using a conditional statement to determine the discount:
Now, let's refactor this with the 'Replace Conditional with Polymorphism' technique. We can start by creating a base Customer class and subclasses for each customer type:
Now, the discount can be determined by the class of the customer object:
This refactored code eliminates the need for conditionals and makes the discount policy for each customer type more explicit and easier to manage. It also adheres to the open/closed principle, which states that software entities should be open for extension, but closed for modification. If we need to add a new customer type in the future, we can simply create a new subclass without modifying the existing code.
Break down complex conditional logic into simpler parts.
The 'Decompose Conditional' refactoring technique is about breaking down complex conditionals into smaller, more manageable pieces. Consider an online bookstore where a different pricing rule is applied depending on whether the date falls within a "summer sale" period.
Here's how the original code may look:
In this example, the getPrice method checks if the given date is within the summer sale period. If it is, it applies the summer price; otherwise, it applies the regular price.
This code can become more difficult to understand as the condition becomes more complex. Here's where 'Decompose Conditional' can come in handy.
Let's refactor the code by breaking down the condition and the related logic into separate methods:
Now, the getPrice method is much cleaner and easier to read, as the complex condition has been encapsulated into the isSummer method, and the pricing logic has been moved to summerPrice and regularPrice methods. This makes the code easier to test and maintain.
There are also simple refactoring techniques, such as renaming variables to make sure their function is obvious, and removing dead code. Dead code removal is another place that code visualization can help–It can be extremely obvious from a code map if you can code that isn’t touched by the rest of your codebase. You can take a simple look at the map and remove the dead code quickly.