Refactoring with IDEA

Refactoring with IDEA

By Eric M. Burke, OCI Principal Software Engineer

February 2002



Note from the Author

I do not have any business relationship or ties to IntelliJ. I have used many Java IDEs and currently use and recommend IDEA because it is the best Java IDE I have seen. The art of refactoring is easy to theorize about but tedious to implement without good tools like IDEA. Hopefully other vendors will follow IDEA's lead in this area.

The current market leader, Borland, has recently implemented a handful of refactorings in JBuilder 6 Professional. To be fair, I have not used this version of JBuilder. The refactorings include the ability to rename packages, classes, methods, and fields, as well as to move classes to new packages. For these operations, all references are updated. JBuilder 6 Professional also supports the ability to search for references and symbol definitions. JBuilder Professional is available from Borland for $999.

IntelliJ IDEA

IntelliJ IDEA is a remarkable Java Integrated Development Environment (IDE) from IntelliJ Software.

IDEA is clearly designed and built for efficient coding. It features an unobtrusive user interface with minimal screen clutter, 100% of its functionality is accessible via keyboard shortcuts, and it is highly configurable. For less than $400, IDEA provides:

There are dozens of tiny productivity features that make IDEA a joy to use. For instance, one can type Ctrl-N followed by the first few letters of any class name, and IDEA immediately jumps to the source code for that class. This is a simple concept that dramatically reduces time wasted drilling down through directory trees looking for files.

Perhaps the most compelling aspect of IDEA is its support for refactoring, a technique for systematically improving the internal structure of an application without changing its external behavior. This article focuses on IDEA's refactoring features.

Introduction to Refactoring

The term refactoring has been in vogue in recent years, due in large part to Martin Fowler's 1999 book titled "Refactoring: Improving the Design of Existing Code", published by Addison Wesley.

In this book, Fowler presents 72 refactorings, assigning each a name like "Move Method" or "Remove Control Flag." Each consists of a short description, an explanation of why the refactoring is useful (the "Motivation"), and step-by-step instructions. Fowler also provides concise code examples for each refactoring.

When viewed individually, refactorings do not offer much in terms of new programming techniques. Take the "Rename Method" refactoring, for instance. Most programmers have renamed a method to make an application more readable. One must look beyond the simple building blocks in order to fully understand the value of refactoring.

The Importance of Testing

Improving the internal structure of a program without affecting external behavior is one key goal of refactoring.

If code that worked before refactoring behaves differently afterwards, the refactoring has failed. Therefore, writing unit tests before refactoring is considered critical. If the tests pass both before and after refactoring, it is much more likely that the changes did not break the application.

Relationship to Extreme/Agile Programming

Refactoring is closely related to the whole culture of agile programming, in which lightweight processes are favored over heavyweight ones. In this approach, programmers do not add new features to applications until they are actually required. Thus, the chance of introducing unnecessary functionality is minimized.

Since the first few iterations of an application may not be feature complete, the code must be refactored as new features are added. With each new feature, programmers should write additional unit tests and perform regression testing to ensure that the refactored code still works.

Refactoring stresses small, incremental changes over time that continually improve the structure of an application. This is in stark contrast to approaches that spend months of up front time developing complex frameworks and infrastructures that may never see the light of day.

The Human Factor

Making code more maintainable is another key goal of refactoring, perhaps more important than anything else. Martin Fowler has great quote on page 15 of the book mentioned above:

Self-documenting code requires fewer comments. Fewer comments means reduced chances for code and its documentation to get out of sync.

Refactoring is all about turning overly complex "spaghetti" into concise, self-documenting code.

A Refactoring Scenario

Suppose that Bob and Steve are working on the same problem. Bob spends four weeks developing a robust, high-quality framework to implement the desired feature. At the end of four weeks, Bob has created a highly reusable software component that is easily maintainable. Perhaps Bob even wrote unit tests to validate his solution.

Steve, on the other hand, knocks out a working solution in two days. Steve's solution is not as clean as Bob's and is not very reusable.

In this scenario, Bob is afraid of refactoring, so he spends a month attempting to perfect his solution. Steve, on the other hand, is able to give his interim solution to the customer or QA group after just a few days.

Now suppose that, upon seeing a partially functioning solution, the customer decides that they really want some other feature instead. Steve, a refactoring ninja, writes a few unit tests then adds those new features to the code. Now that he has concrete feedback from his customers, he also invests a little bit of refactoring time and makes the code more reusable.

At the end of the entire cycle, it is very likely that Steve ends up with a better product because he got it into the hands of customers first.

It is also likely that Bob spent a lot of time perfecting his code, only to find out in the end that the customers wanted something different. While Bob can now spend some time refactoring, he is way behind schedule because he did not involve the customers soon enough.

This is not a slam on customers and end users. Instead, it is an acknowledgement of human behavior.

We often have a pretty good idea of how our software should behave but don't really know until we can take it for a test drive. For this reason, it makes sense to rapidly prototype solutions and then use refactoring to gradually improve the design.

This simple example may also illustrate that unit tests are not the panacea that some present them to be.

Even though Bob may have written unit tests and produced a great piece of software, it is useless if it does not meet the customers' expectations.

The moral of this story is that we should not be afraid to omit features in early iterations of software. We should involve customers as soon as possible, and expect to refactor continually as we add more features and make the code more maintainable. Unit tests are an essential ingredient for successful refactoring and should be introduced as the code evolves over time.

A Few Refactoring Obstacles

Most existing code probably does not have unit tests. Without these tests, it is difficult to refactor with confidence, because changes to the code may break the application. Some argue that "refactoring" without unit tests is not really "refactoring," because it is hard to say if the changes actually work.

Fear prevents many developers from refactoring.

Programmers like to implement "correct" solutions. The idea of writing temporary solutions now in order make faster overall progress is troublesome to many developers, sometimes for good reason.

Many programmers understand that once a program looks like it works, managers are reluctant to let them make internal structural improvements that do not add new features. But simply blaming managers is an oversimplification. For many of us, it just feels wrong to write "throw away" code.

As presented in Fowler's book, refactoring can sometimes appear to be a tedious, labor-intensive process. This is because the individual refactorings are often so trivial that we perform them on a routine basis without thinking much about it. Few developers have the patience to meticulously follow a step-by-step process each time they introduce a parameter to a method, for example.

This is where tools come into play.

Refactoring Tools

Having a good refactoring tool eliminates some of the technical barriers to refactoring.

When a tool renames a method, it is much less likely to break existing code. This is because the tool should be able to quickly find and update all references to the method. Since refactorings performed by automated tools are more reliable, the need to write extensive unit tests is somewhat diminished.

Tools also eliminate the tedious nature of refactoring, making it more likely that programmers will do it.

Without a good refactoring tool, moving a class to a new package could break dozens of other classes in an application. Programmers have to manually fix import statements in each broken class, a time consuming and laborious process.

Instead of suffering through this task, we often settle for less than optimal package, class, interface, method, and field names in our code. A good tool should make renaming effortless and accurate.

Find Usages

The ability to find all usages of classes, interfaces, methods, and fields is critical to any refactoring tool. This is what gives tools the ability to update and change code whenever things are moved or renamed.

IDEA excels at this feature, and it is exposed for use by the developer. Within the tool, developers can click on any symbol and select "Find Usages" from a popup menu. IDEA quickly pops up a list of all references to the item, which can be clicked on to jump to that portion of the code.

As already mentioned, developers can also hit Ctrl-N at any point and type in the name of any class. IDEA immediately finds the class and jumps to its source code. This is perhaps one of the most useful features, because large Java projects typically consist of many hundreds of source files, which are deeply buried in package structures.

These searching capabilities are all lightening fast because IDEA maintains a database of symbols (the parse tree) across the entire application. Searching in most other IDEs is based on full text searches of source code, which is error prone and quite slow. IDEA leverages its database very well to excel in this area.

Supported Refactorings

IDEA 2.5 supports numerous refactorings, most of which are outlined here.

Rename

The Rename refactoring works on class and interface names, method names, and field names.

After the user right-clicks on any of these items and selects Refactor...Rename..., IDEA prompts for the new name. It then locates and updates all usages of the item being renamed.

This is an important refactoring because it makes it possible to effortlessly rename items without fear of breaking existing code.

Consider using an IDE that does not support this refactoring. Changing the name of a method can have profound impact on an application, requiring changes to large numbers of classes. This causes programmers to think twice before renaming methods like setFNm(String name) to something more maintainable like setFirstName(String fName).

When we ignore naming issues throughout a large application simply because the changes are too tedious, we end up with software that is much harder to maintain in the long run.

This and other refactorings present a preview of items that will be changed, giving the programmer a chance to change his or her mind. IDEA is also intelligent enough to recognize methods that are overridden from a base class.

For example, before renaming an overriding method in a subclass, IDEA prompts the user as follows:

If CVS or Visual SourceSafe integration is enabled, IDEA automatically checks out and updates any files that reference the item being renamed. It can also optionally search and update references occurring in comments and strings.

Move

The Move refactoring works with classes, interfaces, and static members.

Static methods and fields can be moved to other classes. Classes and interfaces can be moved to other packages. As with the Rename refactoring, Move updates all existing code that uses the items being moved. Any package statements within the moved files are also updated.

Change Method Signature

This refactoring makes it easy to add, remove, and change parameters to existing methods.

With each new parameter, users must specify a default value. These values are inserted into any existing code that calls the method.

Copy Class

In many cases, programmers copy and paste existing code when creating new classes. The Copy Class refactoring performs a somewhat intelligent copy of any existing class or interface. With this refactoring, existing code is copied and references within the source are also renamed so the copy still compiles.

For instance, if a class is copied to a new package, the package statement is updated. The class or interface declaration is also updated to match the new file name.

Extract Method

The Extract Method refactoring allows programmers to select a block of existing code and replace it with a call to a new method within the current class. The selected code is automatically moved to the newly created method, and IDEA does a reasonably good job of declaring the correct method arguments and return types.

This is a useful refactoring because repeated blocks of code are easily copied to reusable methods. These methods can then be shared by other parts of the application or introduced merely to make the existing method more readable.

Introduce Variable

This refactoring is best explained with an example.

Suppose a program contains the following line of code:

System.out.println(System.getProperty("user.home"));

By highlighting System.getProperty("user.home"), the programmer can use this refactoring to change the code to this:

String userHome = System.getProperty("user.home");
System.out.println(userHome);    

Introduce Field

The Introduce Field refactoring is similar to Introduce Variable; however the developer has much more control over where and how the new field is declared and initialized.

Here is an image showing the options:

Introduce Field

Inline

This refactoring is the opposite of Introduce Variable. When inlining code, someone might start with something like this:

FileWriter fw = new FileWriter("out.dat");
BufferedWriter bw = new BufferedWriter(fw);
PrintWriter pw = new PrintWriter(bw);

After the user clicks on the last bw and selects the Inline refactoring, IDEA produces this:

FileWriter fw = new FileWriter("out.dat");
PrintWriter pw = new PrintWriter(new BufferedWriter(fw));    

The developer can then select the last fw and perform the refactoring again to turn the entire operation into a single statement:

PrintWriter pw = new PrintWriter(new BufferedWriter(
        new FileWriter("out.dat")));

Replace Temp with Query

With this refactoring, a temporary variable holds the result of an expression.

Consider this code fragment:

  1. public void printFullName() {
  2. String fullName = this.firstName + " " + this.lastName;
  3. System.out.println(fullName);
  4. }

After applying this refactoring, the code becomes:

  1. public void printFullName() {
  2. System.out.println(getFullName());
  3. }
  4.  
  5. private String getFullName() {
  6. return this.firstName + " " + this.lastName;
  7. }

Now, the fullName temporary variable is replaced with the getFullName() method, which can be reused by other parts of the class.

Extract Interface

This refactoring allows developers to easily create an interface based on methods in any existing class.

Suppose we have a class called Customer containing several methods. Invoking this refactoring causes the following dialog box to appear:

Extract Interface

After selecting OK, it gets really interesting. IDEA now shows this dialog:

Search Usages

This means that all references to the existing Customer class can be automatically updated to utilize the new Person interface. This update is performed across the entire project.

Extract Superclass

This refactoring is similar to Extract Interface, but more powerful. It allows developers to push existing constructors, methods, and fields from an existing class up to a new base class.

If the existing class already implements some interfaces, IDEA asks whether or not the existing class or the new base class should implement those interfaces.

If the class already contains a base class, this refactoring inserts a new class into the inheritance hierarchy.

Just like Extract Interface, IDEA optionally analyzes and updates all existing code that uses the class, attempting to use the base class whenever possible.

Encapsulate Fields

This refactoring is similar to IDEA's ability to generate getters and setters, which is discussed shortly.

With the refactoring, the user starts by selecting from a list of fields in a class. For each field, IDEA can generate a getter, a setter, or both; users choose the names of these methods.

IDEA can also change the field's visibility, such as making it private.

Convert Local to Field

In this refactoring, a local field within a method is transformed into an object field. The programmer can choose to initialize the field when it is declared, in the constructor, or in the current method.

Convert Anonymous to Inner

Finally, IDEA can turn any anonymous class into an inner class.

Here is some code before applying this refactoring:

  1. ActionListener a = new ActionListener() {
  2. public void actionPerformed(ActionEvent e) {
  3. // ...do something
  4. }
  5. };

And here is the code after:

  1. ActionListener a = new MyActionListener();
  2. ...
  3.  
  4. private class MyActionListener implements ActionListener {
  5. public void actionPerformed(ActionEvent e) {
  6. // ...do something
  7. }
  8. }

Pseudo Refactorings

These additional items are not really refactorings, but they certainly make coding easier:

Let's work through an example.

Suppose we start by writing the following class:

  1. import java.util.Date;
  2.  
  3. public class Customer {
  4. private String firstName;
  5. private String lastName;
  6. private Date birthDate;
  7. }

By the way, developers don't have to type the import statement. IDEA underlines the Date with a red line, much like Microsoft Word does for spelling errors, allowing the user to insert the import statement by hitting Alt-Enter.

Now, IDEA can generate a constructor. To do this, developers hit Alt-Insert and then select "Constructor" from the popup menu. Finally, they select the fields to initialize from this dialog:

Choose Fields to initialize by Constructor

Now the class looks like this:

  1. import java.util.Date;
  2.  
  3. public class Customer {
  4. private String firstName;
  5. private String lastName;
  6. private Date birthDate;
  7.  
  8. public Customer(String firstName, String lastName, Date birthDate) {
  9. this.firstName = firstName;
  10. this.lastName = lastName;
  11. this.birthDate = birthDate;
  12. }
  13. }

To add getters and setters, the user hits Alt-Insert again, then selects "Getter and Setter" from the popup.

After the user selects all of the fields, IDEA generates the remainder of the class:

  1. import java.util.Date;
  2.  
  3. public class Customer {
  4. private String firstName;
  5. private String lastName;
  6. private Date birthDate;
  7.  
  8. public Customer(String firstName, String lastName, Date birthDate) {
  9. this.firstName = firstName;
  10. this.lastName = lastName;
  11. this.birthDate = birthDate;
  12. }
  13.  
  14. public String getFirstName() {
  15. return firstName;
  16. }
  17.  
  18. public void setFirstName(String firstName) {
  19. this.firstName = firstName;
  20. }
  21.  
  22. public String getLastName() {
  23. return lastName;
  24. }
  25.  
  26. public void setLastName(String lastName) {
  27. this.lastName = lastName;
  28. }
  29.  
  30. public Date getBirthDate() {
  31. return birthDate;
  32. }
  33.  
  34. public void setBirthDate(Date birthDate) {
  35. this.birthDate = birthDate;
  36. }
  37. }

IDEA Summary

IDEA is clearly a powerful programmer's editor. It takes the tedium out of refactoring, adding incredibly fast searching capability and keyboard navigation to the mix. Here are some pros and cons of the tool.

Pros

Cons

No tool is without its limitations. IDEA suffers in the following areas:

For many developers, lack of EJB "wizards" is not a significant barrier. For one thing, most of the EJB deployment tools built into market-leading IDEs are specific to particular application servers. Anyone using a different server, such as Orion or JBoss, won't be able to use those features anyway. Anyone who needs an EJB code generator might want to try out the open source XDoclet framework.

Recommendation

IntelliJ IDEA is a "must have" tool. It succeeds where many other IDEs fail: It actually makes developers more productive. Its powerful searching, refactoring, and code-completion capabilities make it a joy to use.

References



Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret