Code Refactoring Step-by-Step Examples. Part V of V Mini-Tech Series
In part I of our refactoring series we talked about what is code refactoring, and why it deserves an investment during development. Part II was all about when to choose to refactor, what situations it's absolutely crucial to refactor in, and our teams take on the clean code approach. In Part III we highlighted consideration points and risks before refactoring and shared techniques to ensure you've got refactoring down.
In Part IV we looked at common signs that signal it's time to refactor. In the final part, we share step-by-step examples, tools, and resources for refactoring.
Make sure to come back every Wednesday for more tech shorts, how-tos, and deep dives into engineering tools and processes.
Ron Jeffries, one of the founders of Extreme Programming software development methodology compares refactoring to clearing a field
" We take the next feature that we are asked to build, and instead of detouring around all the weeds and bushes, we take the time to clear a path through some of them.
However small, large, general or specific the task is, there are dozens of tips and instructions for the best refactoring performance possible. Here are couple of examples.
Refactoring Step by Step Examples
Create a new method by extracting a code fragment from an existing method. In our example, we extract the code that reports on each machine.
Step 1: Create a new method by extracting a fragment from an existing one. We are creating a mini-API here, it's vital to use a clear and proper name to reflect the new method's purpose, not its implementation. If your gut has any doubt, you can check the new method signatures validity by compiling and testing.
Step 2: Copy the code into the new method with a simple copy-paste. In our example, the reportMachine method doesn't compile because of it's temporary variable machine and out parameter. However, the original method still remains unchanged at this stage.
Step 3: For each of the temporary variables used in the copied code, add a parameter to the new method. You'll also need to declare the checked exemption thrown by the write methods.
Note: At each step, we check progress by compiling. You know you're done when the new method compiles cleanly. Since it still hasn't called, the entire application should pass its tests at this stage.
Step 4: Now you can replace the copied code in the original method by a call to a new method. Then compile, test, and you're done.
But sometimes refactoring code isn't so straightforward. Here's a scenario example that's more complex and modifies the result temporary variable.
When the code we want to extract modifies a temporary variable, you'll need to go back to step 3 and declare a new result in the new method and return its value at the end of the computation:
Now in step 4, you'll need to use the returned machine report.
Note: There are plenty of refactoring tools available that automate this implementation.
Introduce Parameter Object
Often in method signatures, you'll see a group of parameters. You can remove some duplication and convert them into a single new domain abstraction.
Here's an example of an application where a robot moves along a row of machines in a production plant.
Note: You might have an object like that together with a client code as String report = Report.report(machines, robot);
If you notice that the list of Machines is often passed around with the Robot, you can parcel them together as a Plant object.
Step 1: Create a new class for the clump values
Start by creating a new Plant class which is a simple container for the two values, and make it immutable to keep things clean.
Step 2: Add Plant as an additional method parameter
Pick any method that takes machines and robot as parameters, then add an additional parameter for the plant. Then change the caller to match like this: String report= Report.report( machines,robot,new Plant(machines,robot));
Then compile and test.
Step 3: Switch the method to use the new parameter
Make the original parameters redundant, one at a time. First, alter the method to get machines from the plant. Then compile, test, and commit here.
Now the machines parameter is unused within the method so we can remove it from the signature.
Every call site:
String report=Report.report(robot,new Plant(machines,robot));
This is another good place to compile, test, and commit. Then do the same with the robot parameter.
String report=Report.report(new Plant(machines,robot));
You can use these steps for every method that has the same two parameters.
Separate Query from Modifier
Methods that have side effects are harder to test and are less likely to be safely reusable, and methods that have side effects in addition to returning a variable also have multiple responsibilities. In most of these cases, it's beneficial to split a method into two parts: separating query and command methods.
In this example, we have a meeting class with a method that looks for a manager in its configuration file and sends them an email.
Note: You should always be testing your code. If you don't have a test-you're leaving a wild card in the system. The code you see here will be testable if you can separate the two responsibilities. Here you can see that the method performs as a query by looking up the manager in the file, and as a command.
Step 1: Create a copy with no side effects.
Create a new method by copying the original and deleting the side effects. As a result, you have a pure query because it is never called, you can compile, test, and commit as needed.
Step 2: Call the new query
With a new query method, you can use it in the original method, and compile and test:
Step 3: Alter the callers
The original method is called here. You can alter this method to make separate explicit calls to the command and query. Do this for all callers of the original method, and since you are not altering the application's overall behavior you can compile, test, and commit at any stage.
Step 4: Void the command method
When you convert all callers to use the command-query separated methods, you can remove the return value from the original method. This makes the method pure command.
Note: You should still be testing here. Callers should still pass their tests.
Step 5: Remove duplication
Use a new query within the command method to remove duplication, and you're done.
Replace Inheritance with Delegation
For extracting a class from an inheritance hierarchy.
Step 1: Create a new field in the subclass to hold an instance of the superclass. Initialize the field to this.
Step 2: Change all calls to superclass methods so that they refer to the new field. Call them via the object referred to in your new field instead of directly calling superclass methods from the subclass, then compile and test.
Step 3: Remove inheritance and initialize the field with a new instance of the superclass.
Step 4: Compile and test.
Step 5: Check if you need to add new methods to the subclass. If its clients use methods from previous inheritances, add in the missing methods, and compile and test.
Remove Control Couple
When a method parameter is used inside the method merely to determine which two or more code paths should be followed. In this method, there are at a minimum two responsibilities, and the caller "knows" which one to invoke by setting the parameter to an appropriate value. Boolean parameters are often used this way.
Step 1: Isolate the conditional. Use the Extract Method to verify that the conditional check and its branches form the entirety of a method.
Step 2: Extract the branches. Use Extract Method on each branch of the conditional so each consists only of a single call to a new method.
Step 3: Remove the coupled method. Use Inline Method to replace all calls to the conditional method, and then remove the method itself.
Product engineer and CTO Andreas Klinger recommends:
" The rule of Fix-it Friday is simple: unless your current project is on fire, use Fridays to invest in little improvement. Let engineers choose what they work on. Try not to take the "fun" out of this by micromanaging. Some will try out new libraries. Some will remove bugs from the backlog. Both are fine. Try encouraging a balance of tasks.
Replace Error Code with an Exception
Sometimes errors can be too cryptic. When special values are returned by methods to indicate an error, thrown in an exception.
Step 1: Should the exception be checked or unchecked? Make it unchecked if the caller(s) should have prevented the error from occurring.
Step 2: Copy the original method and change the new copy to throw the exception in place of returning the special code. Since the new method isn't called yet, compile and test.
Step 3: Change the original method so that it calls the new one. This way the original method catches the exception and returns the error code from its catch block. Once you do this, compile and test.
Step 4: Use Inline Method to replace all calls to the original method with calls to the new method. Then compile and test.
When one object reaches through another to get a third, you need to reduce coupling and improve encapsulation.
Step 1: Create a delegating method on the middle-man object for each method you reach through to call, then compile and test after creating each method.
Step 2: Adjust the client(s) to call the new delegating methods and then compile and test each change.
Step 3: If no one accesses the delegated object via the middle-man, remove the accessor.
Preserve Whole Object
Pass an object instead of the separate values when several of the method's arguments can be obtained from a single object.
Step 1: Add a new parameter for the whole object. Pass the object that contains the values you want to replace, then compile and test.
Note: Since the extra parameter is not used, you compile and test.
Step 2: Pick one parameter and replace references in the method. Replace uses of the parameter with references to the value obtained from the whole object, then compile and test each change.
Step 3: Remove the unused parameter. Also, delete any code in the callers that obtains the value for that parameter, then compile and test.
Step 4: Repeat for every value that can be obtained from the new parameter and compile and test.
Refactoring involves making numerous changes to code and incurs a significant risk of breaking that code. Here are a few tips and resources to help lower the risk and make it an overall lighter process.
- Make sure the code you plan to refactor is working code
- Have an automated test suite with good coverage
- Use version control to save checkpoints before refactoring (recovery/revert)
- Break each refactoring into smaller steps
- Use refactoring tools
Curious how your code is doing? We can perform an assessment and highlight all the areas that are a risk and remedies that can be implemented right away. Chat us online or send us a message.
- Martin Fowler - Refactoring Starting Point Code Example
- Baeldung examples of advanced refactoring
- Wouter Lagerweij step by step refactoring example
- Dzone Refactoring patterns
- Martin Fowlers book paraphrased
- Martin Fowlers Refactoring book on Amazon
- Uncle Bob's Clean Code book on Amazon
- Jeffrey Way video of refactoring multiple levels of code
- Knowthecode video example of refactoring
- GitHub - Jedi autocompletion, static analysis, and refactoring library
- Github - Complete project to show how to apply DDD, Clean Architecture, and CQRS by practical refactoring.
- Refactoring essential for VS
- Code Refactoring Tisp for C#
- VS code refactoring tools
- 5 Best JetBrain Extensions for refactoring
- Andreas Klinger - Refactoring larger legacy codebases