If you have also witnessed your code not feeling as fresh as it should have been, or if you have doubts about any potential glitches or pop-ups, then you are definitely not alone! On the contrary, you are well aware and have already entered the world of ‘Code Smells.’ These are the little tell-tale signs of what might be wrong with your code and what could be done better or paid more attention to.
These signs can be spotted giving a heads-up to show where the code is broken or inefficient so as to enable timely improvisations.
In this article today, we will do an in-depth study of Code Smells. We will discuss types of code smells, their identification, and, most importantly, why they matter and how to spot them easily.
Code smells are indicators that, when detected, source out critical loopholes, missing commands, or issues in the respective application or codebase. Sometimes, these can also predict errors or issues that might arise in the near future, even if there aren’t any currently. These errors can be modified during refactoring as the code smells are easy to detect and fix.
Code smells may also manifest as symptoms of deeper issues within the code. So, even if the code is performing and operating as intended, it may interfere with the development process and pose security or implementation threats while the program or application is running.
Therefore, by definition, a code smell represents an indication of a problem simmering beneath the surface currently or that might originate in the near future. Why the word ‘smell’? Simply because it is easy to spot!
Code smells originate when code created by developers does not meet the required design standards or principles. The reasons behind this can be varied, including lack of understanding or knowledge, time constraints, rushed efforts to meet deadlines, and at times, bypassing code reviews, which can lead to mistakes causing these glitches during development and resulting in code smells.
Code smells come in different kinds and vary based on the project as well as the developer. Here are some of the most common types:
In the example below, the function aims to calculate the sum of even numbers from a given array. However, it is unnecessarily complex and could be simplified using higher-order functions in Swift.
func findSumOfEvenNumbers(numbers: [Int]) -> Int { |
The refactored version is much more concise and easier to understand. It utilizes the filter method to filter out even numbers and the reduce method to calculate their sum. It effectively removes the contrived complexity present in the original implementation.
func findSumOfEvenNumbers(numbers: [Int]) -> Int { |
For example, in the code below; it is unclear what is happening and what these variables are for:
// user data |
Now, it is easier to understand the meaning with better variable names:
// user data |
In the below example, MiddleMan class has two methods, both of which simply delegate functionality to DataManager class. The MiddleMan class does not provide any additional value or functionality. It merely acts as an unnecessary middleman between the client code and the DataManager.
class MiddleMan { |
As a resolution, the MiddleMan Class can be deleted and the DataManager class can be straight away used in the client code. This will be beneficial as it will streamline the codebase and also increase organized efforts.
In the snippet below, the function prints a message but hardcodes the version number into the message. If you need to update the app to a newer version, you would have to modify the function’s code directly, which is not ideal.
func printWelcomeMessage() { |
To avoid this, we should pass the version number as a parameter in the function. Now the function can accept different version numbers without modifying its code.
func printWelcomeMessage(version: String) { |
Like in below example the class has no properties of its own and hence can be recognized as LazyClass. And since it is not adding any value at all to the codebase, it should be removed.
class LazyClass { |
In the example stated below, the subclass Fish does not utilize the behavior it has inherited from its superclass – Animal. This leads to code smell. As a resolution, it is to be ensured that all subclasses use the inherited methods correctly or either override it completely.
class Animal { |
Also read: Simplifying software development by building faster with reusable components
In the below example, the GodObject class has properties to store data related to customers, orders, and inventory. It contains various methods to process and manipulate this data.
class GodObject { |
To address the code smell in the above case, split the class responsibilities.
Like in the example below, a single function is using multiple if-else statements to manage different commands.
func greetUser(timeOfDay: String) { |
Hence, one can use a dictionary to solve this since that is a cleaner approach than if-else statements.
func greetUser(timeOfDay: String) { |
In the below example “createproduct” method has a long list of parameters. This makes the function call cumbersome and hard to read.
class ProductService { |
To address this smell, we can use “struct” or “class” to encapsulate multiple parameter lists into a single entity.
struct Product { |
Here in the below example, person.address.city creates message chains of method calls.
class Person { |
Now, the Person class has a getCity() method that retrieves the city from the Address object. This decouples the code from the internal structure of the Address class.
class Person { |
The below example showcases a calculation where the sum of an integer array is deduced using a loop and indexing. Even though the code works, it also shows an oddball solution smell and thus it cannot be used as a solution for Swift.
func calculateSum(_ numbers: [Int]) -> Int { |
There’s a method to sum an array of integers in Swift. By using the reduce method, the calculation can be completed. This technique is a higher-order function as it will keep on repeating over the elements of the array.
func calculateSum(_ numbers: [Int]) -> Int { |
Below methods are generic and do not hold any specific relation to any actual data type currently being handled:
class DataManager { |
struct User { |
In the above example, the Speculative Generality has been removed during refactoring. A specific UserDataManager class was used for user-related operations. Now, suppose if we have to handle a variety of data, we can create separate managers or classes for respective data types instead of having overly generic methods.
Also read: [Comparison] Android UI Development with Jetpack Compose vs XML
In the below example, Using primitives (Double) for such domain concepts can be a sign of Primitive Obsession, especially if we have multiple functions repeating this pattern or if the concept of a rectangle is used in various places throughout the codebase.
func calculateAreaOfRectangle(width: Double, height: Double) -> Double { |
Here in the below code, by using a custom struct, we address the Primitive Obsession code smell and provide a clearer representation of the rectangle domain concept throughout the codebase. If we need to add more behaviors or properties related to rectangles, we can easily do so within the struct.
struct Rectangle { |
func processOrder(orderId: Int, customerName: String, customerEmail: String, shippingAddress: String, items: [Product]) { |
In the above example, the processOrder takes various parameters under consideration. These factors frequently appear together even when a command happens from multiple places. By grouping related data together using the data structure, we can address the situation of data clump.
struct Order { |
When the switch happens because of an object type, a design pattern is used to frame and outline it for smoother functioning.
func getDayName(_ day: Int) -> String { |
The above can be considered as a code smell because here we are using magic numbers. Also, this code has limited flexibility because if we need to extend a function, it would add additional complexity.
We should use enum instead to avoid this smell.
enum DayOfWeek: Int { |
In the snippet below, it is not apparent why we are checking 10. What is the significance behind this number?
func calculateAreaOfCircle(radius: Double) -> Double { |
In the above example, the value of pi is hard coded as 3.14159. While it’s a valid value for π (pi), it is considered a magic number because its meaning is not immediately apparent to someone reading the code. Instead, it is better to use the built-in constant Double.pi or declare a named constant for it.
func calculateAreaOfCircle(radius: Double) -> Double { |
Also read: Creating Private CocoaPods Libraries
Once the concept of code smells is well understood, it becomes easy to handle them. The removal and tackling of code smells happen through refactoring. Refactoring can be seen as a complete makeover of the code, which is complete with sweeping, decluttering, and streamlining.
Recognizing the need for refactoring
Spotting a code smell is a major task. Once that is done, look at the indicator and understand the location of the code smell. So the initial step becomes recognition. The acknowledgment of code smells’ existence marks a major work done.
Understand the code smell
Once the code smell has been identified, another milestone to cover is recognizing its category or belonging. Maybe it is a duplication or a piece of code, which is kept for future use. Maybe there is a function that is too large. Once we know exactly what we are dealing with, it becomes easy to refactor it efficiently.
Plan your approach
Refactoring is not a process that can be completed hurriedly. It requires a methodological approach that demands thought and deliberation. Before taking any step, plan ahead – break down the problem into doable tasks, outline your goals, and find out the best way to solve the issue behind the code smell.
Make small, incremental changes
Refactoring does not necessarily mean that your entire code is to be written again. It only means that once the issues have been identified, changes need to be made to the code. These changes are localized to the origin of the code smell and are manageable. This approach reduces the risk of bugs and also maintains full functionality throughout the process.
Utilize design patterns
Design patterns serve as tested templates for perfect code. They offer varied solutions to common problems, helping in the process of refactoring. Whether it is the Singleton pattern, Factory pattern, or Observer pattern, implementing these patterns can prove extremely advantageous in removing code smells.
Leverage automated tests
Refactoring the first stage is always followed by testing. Anything that goes without testing cannot guarantee accuracy. Automated tests guarantee safety and lower the amount of risks. They also ensure that your changes don’t break current functionality. Hence, before beginning the refactoring activity, it is to be ensured that testing mechanisms and plans are active.
Iterate and review
Refactoring is not a one-step process. It happens in iteration. After introducing any change, review the code. Test its functionality and check for optimization. One can always take a second opinion to review the code as well and ensure its functionality.
Document your changes
It is important that documentation of every step is done with clarity and precision. During the refactoring process, put in comments wherever necessary to showcase the changes. This leads to clear understanding as well as make it easy to collaborate.
Measure the impact
Once refactoring is completed, evaluation must begin. It will help in gaining insights into the impact of changes made. One can understand if the code smells have been removed or not. It will also show if the code is now clean and easy to manage. Performance improvements and bug reduction can also be observed.
Here, we are sharing some tips that new developers can follow to avoid code smells. If one follows these tips, the code can remain clean throughout, and the chances of code smell arising will be low.
Guideline | Description |
---|---|
Clear Planning | Draft a clear plan before you begin coding. It is important to understand the problem first and then design your solutions as per the required customization. Any hasty coding should be avoided. |
Follow Naming Rules | Meaningful names should be used for variables, functions, and classes. Clear names are like guidelines for readers and users. |
Avoid Copy-Pasting | One must avoid copying, and instead create functions that can be reused or classes to lower the risk of duplicate code. |
Keep Functions Brief | The length of the functions should be kept brief and concise. Avoid unnecessary long codes. It not only adds to the weight of the code but also makes it susceptible to bugs. Long codes can be broken down into smaller fragments. |
Limit Parameters | Try to avoid lengthy parameter lists. Similar or related data can be merged or grouped together into structs or classes. |
Regular Code Reviews | Regular reviews can detect code smells early. One can get inspired by experienced developers. |
Future-Ready Code | Changes are bound to come. Hence, write flexible codes for easy and quick iterations. |
Meaningful Comments | Comments are helpful as they detail complex logic, assumptions, and decisions. |
Test Thoroughly | Automated tests catch bugs, and security threats and also support refactoring. |
Learn from Experts | Read and study well-written code in open-source projects. Learn to apply their techniques. |
Creating clean code takes time. To master this subject, always keep learning and practicing.
Also read: [Guide] Creating Charts with Swift Charts
In this blog, we have learned about the importance of code smells in detail and discovered their inevitability during the course of software development. We have also gained an understanding of the various types of code smells and their impact.
It is important to note that code smells serve as early indicators of potential problems, and all these issues can be easily solved through refactoring. If we address them quickly and also utilize best practices, then we can make our code strong, bug-free, and adaptable over time.
At Aubergine Solutions, we understand and reaffirm the importance of clean code and efficient solutions. Our skilled developers create top-notch software that is designed to meet your current requirements and remain effective over time. Connect with us today to find out more about our software development services and how we can collaborate with you on your next project.