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.

techdebt

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

Extract Method

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. 

Code Example

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. 

Screen Shot 2022-05-10 at 10.03.20 AM

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. 

Code Example 1

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.

Screen Shot 2022-05-10 at 10.05.35 AM

Checked exemption: 

Code Example 1

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. 

Screen Shot 2022-05-10 at 10.07.54 AM

But sometimes refactoring code isn't so straightforward. Here's a scenario example that's more complex and modifies the result temporary variable.

Screen Shot 2022-05-10 at 10.18.35 AM

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: 

Screen Shot 2022-05-10 at 10.08.23 AM

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.

Screen Shot 2022-05-10 at 10.08.37 AM

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. 

Screen Shot 2022-05-10 at 10.37.04 AM

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. 

Screen Shot 2022-05-10 at 10.37.16 AM

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.

Screen Shot 2022-05-10 at 10.37.26 AM

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. 

Screen Shot 2022-05-10 at 10.37.46 AM

Now the machines parameter is unused within the method so we can remove it from the signature. 

Screen Shot 2022-05-10 at 10.37.57 AM

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));

Screen Shot 2022-05-10 at 10.38.08 AM

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.

Code Example 3

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.

Code Example 5

Step 2: Call the new query

With a new query method, you can use it in the original method, and compile and test:

Screen Shot 2022-05-10 at 10.24.08 AM

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.

↓
Screen Shot 2022-05-10 at 10.25.40 AM

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.

Code Example 6

Step 5: Remove duplication

Use a new query within the command method to remove duplication, and you're done.

Example cred: Dzone

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.

refactoring

Hide Delegate

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 Tips

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
Refactoring Lang - Tools

Leave a Comment