This article is a revised version. It was previously published at refactoring-legacy-code.net published.
This is a series of articles. You can find the other articles here:
- Simple refactorings - Part 2
- Simple refactorings - Part 3
- Simple refactorings - Part 4
- Simple refactorings - Part 5
Simple vs. complex refactorings
In this series of blog posts, I will introduce you to so-called Simple refactorings before. In contrast to Complex refactorings these are fully tool-supported. This means that you make the changes to the source code automatically using an IDE, e.g. Microsoft Visual Studio or JetBrains Rider. You may also use an additional refactoring tool such as JetBrains ReSharper. Using such a tool preserves the behavior of the application during refactoring. As long as you consistently carry out the refactorings exclusively with such tools and do not intervene manually, there is a very high probability that the code will behave exactly as it did before the refactoring.
Complex refactorings on the other hand, require "manual work", i.e. intervention without the certainty that the semantics will be preserved through the use of tools. Furthermore Complex refactorings usually so extensive that the effects are not foreseeable from the outset. Typically, you start at one point and realize that more and more effects are added that you had not foreseen at the beginning. Very quickly you are left with numerous loose ends and don't know whether the application will still work. Or you know that something no longer works, but you have no idea which of the changes led to this.
The goal of simple refactorings
Simple refactorings are usually used to improve the Readability and thus the Comprehensibility of the code. The aim is to Changeability of the code or to improve it. It is not first and foremost a matter of Patterns, i.e. design patterns. Refactoring is not an end in itself, but must bring benefits to the customer or the company. This benefit is the changeability of the software. If a pattern serves the purpose of changeability, there is no reason not to use it. However, the pattern is not the primary goal, but changeability.
Improve readability - Rename Refactoring
One of the simplest and yet most important refactorings to improve readability is the renaming of symbols. Whether it is a Variablea Parametersa Fielda Featurea Method or a Class is: a good name conveys the meaning and thus increases the comprehensibility of the code.
In addition, names in a code base must adhere to a uniform style, as this also improves readability. Code conventions define how, for example, variables, parameters and fields are named for a software system. In addition to a standardized Domain language it must be determined whether and how, for example, variables are distinguished from fields by a convention. For example, fields can be named with a preceding underscore so that they are clearly distinguished from local variables and parameters within methods. Whatever your specific code convention looks like, it is important that your team agrees on one and then adheres to it consistently in the code. In places where the convention has not been adhered to, the Rename Refactoring.
An example
In the following example, the fields item1 and item2 cannot be distinguished from the parameters of the constructor. Therefore, the preceding this to clarify which identifier is meant.
public class Pair { private T1 item1; private T2 item2; public Pair(T1 item1, T2 item2) { this.item1 = item1; this.item2 = item2; } }
There is no reason not to leave the code as it is, as it is syntactically correct and the meaning is also clear. A code convention such as "Names of fields of a class begin with an underscore" could be used to achieve unambiguity in another way. The team should simply agree on a convention in order to achieve uniformity in the code base and thus increase readability. After a rename refactoring on the item1 and item2 fields, the example would then look as follows:
public class Pair { private T1 _item1; private T2 _item2; public Pair(T1 item1, T2 item2) { _item1 = item1; _item2 = item2; } }
The advantage of the tool-supported Rename Refactorings The advantage of this over changing "by hand" in the editor is that the IDE ensures in this way that all usage locations of the symbol are adapted. This prevents errors from creeping in. Of course, the IDE also ensures that identifiers remain unique when renaming. If you try to name a symbol after an existing one, you will be notified that the name already exists.
If you find it difficult to find a good name for a functional unit, which can be a method or class, this could be an indication that the task of the functional unit is unclear. Aspects are often mixed up so that you would like to use the conjunction "and" in the name of the functional unit. Through a Rename Refactoring the actual problem, the mixing of aspects, cannot be solved. I will come back to this problem later.
Conclusion
Use the Rename Refactoringto adapt the identifiers in your codebase to your Code conventions and to use appropriate terms from the Domain language to use. This increases the Readability and Comprehensibility of the code and thus leads to better Changeability.
Improve readability - Introduce Variable
It is often useful to store an expression in a variable in order to use the option of assigning a further name and thus designating the expression. In this way, the meaning of the expression can be named and the reader does not have to work it out for themselves by reading the expression.
public double Endpreis(int anzahl, double netto) { return (number * net) * 1.19; }
Here, the introduction of variables can help to understand the calculation more quickly:
public double Endpreis(int anzahl, double netto) { var netTotal = number * net; var grossTotal = netTotal * 1.19; return grossTotal; }
And of course this refactoring can be continued in order to name the magic number "1.19":
public double Endpreis(int anzahl, double net) { const double mwSt = 1.19; var netTotal = number * net; var grossTotal = netTotal * VAT; return grossTotal; }
Another typical scenario for the introduction of a variable is nested method calls. The following example shows three nested method calls. To understand the code, it is necessary to read it from the inside out, i.e. from right to left.
public IDictionary ToDictionary(string configuration) { return InsertIntoDictionary(SplitIntoKeyValuePairs(SplitIntoSettings(configuration))); }
As developers, we have some practice in reading these nested calls. And yet it does not correspond to our usual reading flow, which is from top to bottom and from left to right. To arrange the calls one after the other, select the individual calls and start the Introduce Variable Refactoring. The result is the following version of the method:
public IDictionary ToDictionary(string configuration) { var settings = SplitIntoSettings(configuration); var pairs = SplitIntoKeyValuePairs(settings); var result = InsertIntoDictionary(pairs); return result; }
Now the method ToDictionary easier to read and therefore easier to understand.
Conclusion
Use Introduce Variablein order to Expression or a Interim result one Designation to give. By introducing additional identifiers, the reader can understand the Meaning of the code easier. Through Introduce Variable you can also Resolve nested callsso that the code can be read from top to bottom and from left to right.