Some Best-Practices for Identifying and Refactoring Code Smells
Basically, a Code Smell is any characteristic in the source code, which probably represents a deeper issue. Martin Fowler believes that code smells are not problematic issues on their own. They are just warning signals that possibly indicate real problems in the code. Besides, refactoring can be defined as a technique to restructure code in a disciplined way without changing its external behavior; nevertheless, it can be useful for improving non-functional quality, as well as addressing code smells. This articles aims to discuss some best-practices for identifying and refactoring code smells.
Introduction and Overview
Fundamentally, a code smell is any characteristic in the source code, which perhaps shows a deeper issue. Specifying what is and is not a code smell is subjective because it can be different by considering language, developer, and development methodology. Even though this term was first popularized by Kent Beck in the late 1990s, the usage of the term grew in practice after mentioning it by Martin Fowler in his book. Martin Fowler believes that code smells are not problematic issues on their own. They are just warning signals that possibly indicate real problems in the code. For instance, long functions can be assumed as code smell, but not all long functions are necessarily poorly designed. So, a code smell is a hint that something probably be wrong, not a certainty.
Refactoring (verb): to restructure software by applying a series of refactorings without changing its observable behavior.
Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
— — Martin Fowler
Furthermore, refactoring can be defined as a technique to restructure code in a disciplined way without changing its external behavior; however, it can be helpful for improving non-functional quality, including simplicity, flexibility, understandability, and performance.
If someone says their code was broken for a couple of days while they are refactoring, you can be pretty sure they were not refactoring.
Initially, refactoring applies a series of standardized basic micro-refactoring, each of which is a small change in a source code that either preserves the behavior of the software, or at least does not alter its functional requirements. Refactoring can eliminate code smells, but is much more than this matter. In fact, it can also be beneficial to readability, extensibility, and maintainability of source code.
You have to refactor when you run into ugly code — but excellent code needs plenty of refactoring too.
The whole purpose of refactoring is to make us program faster, producing more value with less effort.
Besides, Martin Fowler believes there are no set of metrics for when refactoring could be done due to informed human intuition. They just only give you indications for some problems that can be solved by refactoring techniques.
Lastly, the important point is that although there are a number of valuable automated tools and IDEs for offering refactoring as fast as possible, these tools are not essential for software developers. You can rely on performing small steps, and using frequent testing to identify mistakes in your code. You can also identify code smells and refactor them (in some situations) with senior members of your development team for teaching people in the team to be better developers.
Duplicated Code
Essentially, duplicated code can be seen as one of the worse code smells. Duplication means that every time you read these copies, you require to read them carefully to see if there is any difference or not. It can also make code much more difficult to maintain. If you have duplicate code, you should remember the DRY principle (Don’t repeat yourself). You can also use various refactoring approaches in this case:
The DRY principle is stated as “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system”.
- If you have the same expression in two functions (methods) of the same class, you can use Extract Function technique and invoke the code from both places.
- If you have code that is similar, not quite identical, you can be able to apply Slide Statements approach.
- When your subclasses have methods that do similar work, you can have a chance to utilize Pull Up Method for making the methods identical and shifting them to the relevant superclass.
Long Function
Long functions can make difficult to read and understand code in practice. A function or method should only ever do one thing. This means it should be short. In most cases, a function should not be more than one screen height long; that is somewhere between 24 and 50 lines long. In addition to this important factor, naming is a key to make functions easy to read and understand because if you have an appropriate name for a function, you mostly do not require to look at its body. Notwithstanding the fact that all you must do to shorten a function is using Extract Function technique in ninety nine percent of the time, you can have an opportunity to apply other refactoring methods in some special cases:
- If you have a function with many parameters and temporary variables, you can often use Replace Temp with Query to remove the temps, and Introduce Parameter Object and Preserve Whole Object techniques can apply to diminish the long lists of parameters. For example, Introduce Parameter Object can be explained as when your methods consist of a repeating group of parameters, you should replace these parameters with an object. If you have considered these in your code and you still have a lot of temps and parameters, you should make use of Replace Function with Command.
- If you have conditionals and loops in your functions, typically, it can be assumed as signs for extraction. So, to tackle conditional expressions, you can use Decompose Conditional. If there is more than one switch statement in your code, you should apply Replace Conditional with Polymorphism. Also, while your loops are doing more than one task, you should separate them with Split Loop.
Long Parameter List
A number of long list parameters determines that the function has faced with too many responsibilities. Thus, the parameter list should be simplified. To decrease parameter list sizes, classes can be noted as an effective way, in particular, when multiple functions share some parameter values. Next, you can utilize Combine Functions into Class to receive those common values as fields. In addition, if a parameter is used as a flag to dispatch various behavior, you can apply Remove Flag Argument. Another refactoring technique for applying in this area is that instead of getting different values from an object and pass them as parameters to a function, you can accomplish it properly by passing the whole object, which is called Preserve Whole Object.
Divergent Change
Basically, Divergent Change is occurred when a number of changes are made to a single class. That means when you want to change to a class, you find yourself having to change a lot of irrelevant functions. The main reason for dealing with this problem is that an inappropriate program structure has been implemented by developers. Hence, to address this problem efficiently, you can utilize several refactoring methods based on your situations, particularly Extract Function, Move Function, and Extract Class.
Shotgun Surgery
Shotgun surgery is similar to Divergent Change, but is the opposite smell. This means making one change needs to change multiple classes in the code at the same time. In other words, making any modification needs you make a lot of tiny changes to many various classes in reality. So, you can apply Move Function and Move Field to move existing class behaviors into a single class. Otherwise, when there is no proper class for that, you should create a new one. Another helpful tactic for Shotgun Surgery is to use Inline refactoring, such as Inline Function or Inline Class.
Feature Envy
In modularization of a program, Feature Envy can occur when a function in one module spends more time on communicating with functions or data inside another module than it does within its own module. However, you can typically see it when a function accesses the data of another object more than its own data. This smell probably occur after fields are moved to a data class. In this case, you should move the operations on data to this class to keep them in the same place. So, you can refactor your code with Move Function and Extract Function based on your case.
Data Clumps
In fact, sometimes different parts of the code can include similar groups of variables, like parameters for connecting to a database. These clumps should be turned into their own classes. The first step is considering where the clumps appear as fields. Then, you should use Extract Class on the fields to turn the clumps into an object. Second, you should turn your attention to method signatures using Introduce Parameter Object or Preserve Whole Object. If you want to ensure whether some data is a Data Clump or not, you should just delete one of the data values and observe whether the other values still make sense or not. As a result, If they do not, this group of variables should be combined into an object.
A good test is to consider deleting one of the data values. If you did this, would the others make any sense? If they don’t, it’s a sure sign that you have an object that’s dying to be born.
Message Chains
You can notice Message Chain when a client requests another object, that object requests another one, and so on. These chains mean that the client is dependent on navigation along the class structure. Therefore, any change in these relationships need to modify the client. You can have an opportunity to refactor your code with Hide Delegate, Extract Function, and Move Function based on your special case. These approaches could also be useful for diminishing dependencies in your code.
Middle Man
As you know. one of the central features of objects is encapsulation, hiding internal details from the rest of the world. Encapsulation often comes with delegation. The key idea is that if a class does just only one task, delegating work to another class, why does it exist at all? Thus, you can use Remove Middle Man as a refactoring tactic in this area.
Comments
Comments are generally written when a developer realizes that his or her code is not obvious to understand. Nevertheless, many software developers believe that comments as a code smell since quality code should be self-explanatory. As a matter of fact, the best comment is a proper name for a method or class. That means comments are no longer necessary in most situations.
When you feel the need to write a comment, first try to refactor the code so that any comment becomes superfluous.
A good time to use a comment is when you don’t know what to do. In addition to describing what is going on, comments can indicate areas in which you aren’t sure. A comment can also explain why you did something.
If you feel that a piece of code cannot be understood without comments, you should try to change the code structure in a way that makes comments not needed. So, you can be able to make use of several refactoring techniques, such as Introduce Assertion, Change Function Declaration, and Extract Function.
Refused Bequest
Obviously, subclasses get to inherit the methods and data of their parents. However, the question is what if they do not require what they are given? If a subclass uses only some of the methods and properties inherited from its parents, the main purpose of inheritance, reusability, cannot be achieved because the superclass and subclass are completely different. When inheritance cannot be useful in practice, the subclass really does have nothing in common with the superclass, you should refactor inheritance via Replace Superclass with Delegate and Replace Subclass with Delegate.
Speculative Generality
In some cases, code is built “just in case” to provide anticipated future, and these features never get implemented and used. Hence, code becomes difficult to comprehend and maintain. For example, for eliminating unused abstract classes, you can use Collapse Hierarchy. Also, you can apply Remove Dead Code when the only users of a function or class are test cases. Unnecessary delegation can be removed with Inline Function and Inline Class. Eventually, You should also apply Change Function Declaration to remove any unused parameters.
In Conclusion
As you noticed, a Code Smell is a hint that something perhaps be wrong, not a certainty. Moreover, refactoring can be defined as a technique to restructure code in a disciplined way without changing its external behavior; however, it can be useful for enhancing non-functional quality, as well as addressing code smells. This essay discussed some best-practices for identifying and refactoring code smells, mainly, based on Martin Fowler’s documents and resources.