Perhaps I am writing about something obvious to many, but maybe not to everyone. Refactoring is, I think, a complicated topic because it involves changing the code without affecting its operation.
Therefore, it is incomprehensible for some that refactoring is actually an area of programming, and it is also a very important part of the programmer’s work. The code is ever-evolving, it will be modified as long as there is the possibility of adding new functionalities. However, it may take a form that no longer allows for effectively adding new functionalities and it would be easier to rewrite the entire program.
What is refactoring?
Usually, the answer you hear is that it is changing the structure of the code by applying a series of refactoring transformations without affecting the observable behavior of the code. This is true. Recently, I also came across a definition by Martin Fowler in his book “Improving the Design of Existing Code” where he describes refactoring as “making big changes in small steps.” He describes refactoring as a code change not affecting its operation, but he emphasizes that it has to be done in small steps.
The book also advocates that refactoring does not affect the operation of the code and points out that it does not have any effect on passing the tests at any time. It describes step by step how to safely perform refactoring. I liked his book because it describes simple tricks that can be used in everyday work.
Why do we need refactoring?
Most often, you may need it when you want to introduce a new functionality and the code in its current version does not allow it or it would be more difficult without changes to the code. Also, it is useful in cases when adding more features is unprofitable time-wise, that is, it would be faster to rewrite the code from scratch. I think it is sometimes forgotten that refactoring can make the code cleaner and more readable. Martin writes in his book how he performs refactoring when he feels unpleasant odors in the code and, how he puts it, “it always leaves room for the better”. And he surprised me here by seeing refactoring as an element of everyday code work. For me, the codes are often incomprehensible, reading it is a bit of experience as the code is often unintuitive.
The distinguishing feature of a well-designed program is its modularity, thanks to which it is enough to know only a small part of the code to introduce most modifications. Modularity also makes it easier for new people to get in and start working more efficiently. To achieve this modularity, related program elements must be grouped together, the connections being understandable and easy to find. There is no single rule of thumb as to how this can be done. As you know and understand more and more of how the code is supposed to work better, you can group the elements, but sometimes you also have to test and check.
One of the rules of refactoring in YAGNI, it is an acronym for ‘You Aren’t Gonna Need It’ and stems from eXtreme Programming (XP) used mainly in Agilesoftware development teams. Long story short, YAGNI says that only up-to-date stuff should be done. This basically means that even if something might be needed in the future, it shouldn’t be done right now. But we also cannot crush further extensions and this is where modularity becomes important.
When talking about refactoring, one of the most essential elements, i.e., tests, must be mentioned. In refactoring, we need to know that the code still works, because refactoring does not change how it works, but its structure, so all tests must be passed. It is best to run tests for the part of the code we are working on after each small transformation. It gives us a confirmation that everything works as it should and it shortens the time of the whole operation. This is what Martin talks about in his book – run tests as often as possible so as not to take a step back and waste time looking for a transformation that broke something.
Code refactoring without testing is a pain and there’s a big chance that something will go wrong. If it is possible, it would be best to add at least some basic tests that will give us a little reassurance that the code works.
The transformations listed below are only examples but they are really helpful in daily programing:
Function extraction and variable extraction – if the function is too long, check if there are any minor functions that could be extracted. The same goes for long lines. These transformations can help with finding duplications in the code. Thanks to Small Functions, the code becomes clearer and more understandable,
Renaming of functions and variables – using the correct naming convention is essential to good programming. Variable names, when well-chosen, can tell a lot about the code,
Grouping the functions into a class – this change is helpful when two classes perform similar operations as it can shorten the length of the class,
Overriding the Nested Statement – if the condition checks out for a special case, issue a return statement when the condition occurs. These types of tests are often referred to as the guard clause. Replacing a Nested Conditional Statement with an Exit Statement changes the emphasis in the code. The if-else construct assigns equal weight to both variants. For the person reading the code, it is a message that each of them is equally probable and important,
Introducing a special case – if you use some conditions in your code many times, it may be worth creating a separate structure for them. As a result, most special-case checks can be replaced with simple function calls. Often the common value that requires special processing is null. Therefore, this pattern is frequently called the Zero Object. However, this approach may be used in any special case,
Replacement of the Conditional Instruction Polymorphism.
Example
This is an article about refactoring and an example is needed. I want to show a simple refactoring sample below with the use of Overriding the Nested Statement and Replacement of the Conditional Instruction Polymorphism. Let’s say we have a program function that returns a hash with information on how to water plants in real life. Such information would probably be in the model, but for this example, we have it in the function.
def watering_info(plant)
result = {}
if plant.is_a? Suculent || plant.is_a? Cactus
result = { water_amount: "A little bit " , how_to: "From the bottom", watering_duration: "2 weeks" }
elsif plant.is_a? Alocasia || plant.is_a? Maranta
result = { water_amount: "Big amount", how_to: "As you prefer", watering_duration: "5 days" }
elsif plant.is_a? Peperomia
result = { water_amount: "Dicent amount",
how_to: "From the bottom! they don't like water on the leaves",
watering_duration: "1 week" }
else
result = { water_amount: "Dicent amount",
how_to: "As you prefer",
watering_duration: "1 week"
}
end
return result
end
The idea is to change if to return:
if plant.isa? Suculent || plant.isa? Cactus
result = { wateramount: "A little bit " , howto: "From the bottom",
To
return { water_amount: "A little bit " , how_to: "From the bottom",watering_duration: "2 weeks" } if plant.is_a? Suculent || plant.is_a? Cactus
return { wateramount: “A little bit ” , howto: “From the bottom”,wateringduration: “2 weeks” } if plant.isa? Suculent || plant.is_a? Cactus
And so on with everything, until we come to a function that looks like this:
def watering_info(plant)
return result = { wateramount: "A little bit " , howto: "From the bottom", wateringduration: "2 weeks" } if plant.isa? Suculent || plant.is_a? Cactus
return result = { wateramount: "Big amount", howto: "As you prefer", wateringduration: "5 days" } if plant.isa? Alocasia || plant.is_a? Maranta
return result = { water_amount: "Dicent amount",
howto: "From the bottom! they don't like water on the leaves",
wateringduration: "1 week" } if plant.is_a? Peperomia
return result = { water_amount: "Dicent amount",
how_to: "As you prefer",
watering_duration: "1 week"
}
end
At the very end, we already had a return result. And a good habit is to do this step by step and test every change. You could replace this if block with a switch case and it would look better immediately, and you would not have to check all ifs every time. It would look like this:
def watering_info(plant)
swich plant.class.to_string
case Suculent, Cactus
{ wateramount: "A little bit " , howto: "From the bottom", watering_duration: "2 weeks" }
case Alocasia, Maranta
{ wateramount: "Big amount", howto: "As you prefer", watering_duration: "5 days" }
case Peperomia
{ water_amount: "Dicent amount",
how_to: "From the bottom! they don't like water on the leaves",
watering_duration: "1 week" }
else
{ water_amount: "Dicent amount",
how_to: "As you prefer",
watering_duration: "1 week” }
end
end
And then you can apply the Replacing the Conditional Instruction Polymorphism. This is to create a class with a function that returns the correct value and switches them in their proper places.
class Suculent
...
def watering_info()
return { wateramount: "A little bit " , howto: "From the bottom", watering_duration: "2 weeks" }
end
end
class Cactus
...
def watering_info()
return { wateramount: "A little bit " , howto: "From the bottom", watering_duration: "2 weeks" }
end
end
class Alocasia
...
def watering_info
return { wateramount: "Big amount", howto: "As you prefer", watering_duration: "5 days" }
end
end
class Maranta
...
def watering_info
return { wateramount: "Big amount", howto: "As you prefer", watering_duration: "5 days" }
end
end
class Peperomia
...
def watering_info
return { water_amount: "Dicent amount",
how_to: "From the bottom! they don't like water on the leaves",
watering_duration: "1 week" }
end
end
class Plant
...
def watering_info
return { water_amount: "Dicent amount",
how_to: "As you prefer",
watering_duration: "1 week" }
end
end
And in the main watering_infofunction, the code will look like this:
def watering_info(plant)
plant.map(&:watering_info)
end
Of course, this function can be removed and replaced with its content. With this example, I wanted to present the general pattern of refactoring.
Summary
Refactoring is a big topic. I hope this article was an incentive to read more. These refactoring skills help you catch your bugs and improve your clean code workshop. I recommend reading Martin’s book (Improving the Design of Existing Code), which is a pretty basic and helpful set of rules of refactoring. The author shows various transformations step by step with a full explanation and motivation and tips on how to avoid mistakes in refactoring. Due to its versatility, it’s a delightful book for frontend and backend developers.