In this article, we will learn what anti-patterns belong to the group of code smells called bloaters and different techniques for refactoring bloated code in C#.
Let’s dive in!
What Are Bloaters?
This category of code smells describes a situation where the code base gets bigger and bigger, even though there is no need for it. The most common cause is the development of existing code. We are adding more features and classes start to lose their original cohesiveness. It is worth remembering that the less code we have, the easier it is to maintain and extend it in the future. On the other hand, it does not mean that we should try to write the shortest possible code, sacrificing readability.
Among bloaters, we can distinguish:
- Primitive obsession
- Long method
- Large class
- Long parameter list
- Data clumps
Let’s look at how to spot them in a code base and what the remedies are!
Primitive Obsession
Simply put, this code smell is about using primitive types instead of specially created types. Primitive obsession can refer to just one variable or to a whole group that should be moved to a common type. It leads to worse readability and worse organization of the code or difficulties in finding duplicates.
Let’s examine the signs of primitive obsessions and possible treatments.
Replace Data Values with Objects and Preserve the Whole Object
Let’s create a Person
class as an example which we can use to refactor:
public class Person { public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; private set; } public string LastName { get; private set; } public string? StreetName { get; private set; } public string? City { get; private set; } public string? PostalCode { get; private set; } public string? Region { get; private set; } public string? Country { get; private set; } public void AssignAddress(string streetName, string city, string postalCode, string region, string country) { this.StreetName = streetName; this.City = city; this.PostalCode = postalCode; this.Region = region; this.Country = country; } }
At the first glance, the code looks correct, but let’s see if we can improve it somehow.
Properties describing the user’s address, for example, StreetName
or City
, should be moved to a separate class. If we had any methods to modify the fields related to the address, we should move them as well. In this way, we get a more logical division and a cleaner code.
The second problem is the AssignAddress
method, or rather its parameters. This is a perfect representation of primitive obsession. We use many primitive types when we could replace them with a single object.
Let’s add the Address
class to group these primitive types:
public class Address { public string? StreetName { get; set; } public string? City { get; set; } public string? PostalCode { get; set; } public string? Region { get; set; } public string? Country { get; set; } }
Now we can adjust the AssignAddress()
method accordingly:
public class Person { public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; private set; } public string LastName { get; private set; } public Address? Address { get; private set; } public void AssignAddress(Address address) { this.Address = address; } }
As a treatment, we replaced data values with an object (the creation of the Address
class) and we preserved the whole object (by passing it as a parameter instead of using many separate primitive types).
Replace Type Code with Class
We can also speak of a primitive obsession when we hold complex data in primitive variables. In this case, it is important to distinguish whether said data affects the program’s behavior, for example, whether it is used in conditions.
With this knowledge, we can choose how this code smell will be fixed. If the data is used only for information purposes and does not affect the operation of the program, we should think about using the replace type code with class technique.
In our example, we created a User
class with a SubscriptionCode
property. The user can have one of the three available subscription plans, marked with the value 0, 1, or 2:
public class User : Person { public static int Free = 0; public static int Family = 1; public static int Premium = 2; public User(string firstName, string lastName, int subscriptionCode) : base(firstName, lastName) { SubscriptionCode = subscriptionCode; } public int SubscriptionCode { get; private set; } }
The solution has several significant disadvantages. For example, it is possible to pass an invalid value for which there is no plan. In addition, we don’t use the benefits of object-oriented programming. By converting numeric code types into an object we could transfer the business logic inside the appropriate class. As a result, we will also enable our IDE to suggest appropriate values and check whether the values provided are correct.
Let’s create a separate object Subscription
and use it instead of numeric code types:
public class User : Person { public User(string firstName, string lastName, Subscription subscription) : base(firstName, lastName) { Subscription = subscription; } public Subscription Subscription { get; private set; } } public class Subscription { public static Subscription Free = new(0); public static Subscription Family = new(1); public static Subscription Premium = new(2); private int Code { get; } private Subscription(int code) { Code = code; } public override bool Equals(object? obj) { if (obj == null) return false; return Code == ((Subscription)obj).Code; } public override int GetHashCode() { return Code.GetHashCode(); } }
The additional advantage of this approach is that we encapsulate the magic numbers completely inside the class.
It’s worth mentioning why we left the Code
property. We need it for the Equals
function as well as when using the GetHashCode
method to compare two types.
Replace Type Code with Subclass
An alternative to the replace type code with the class method is to replace the type code with a subclass. We should use this treatment when we want to use a property in conditions.
But here’s the catch as well!
This solution will work if the type code is immutable (it cannot change during the life cycle of the object). Changing a subclass to another could cause us a lot of problems in the future, so for this case, we will discuss one more solution later.
Let’s assume that there are 3 types of employees in the company: administrator, content writer, and developer. The easiest way to save the role is by using a numeric type code:
public class Employee : Person { public const int Administrator = 0; public const int ContentWriter = 1; public const int Developer = 2; public Employee(string firstName, string lastName, int type) : base(firstName, lastName) { Type = type; } public int Type { get; private set; } }
We can quickly figure out what the problem is by wanting to add a method that returns a monthly bonus for each employee according to his role in the company:
public double GetMonthlyBonus(double monthlySalary) { if (Type == ContentWriter) { return monthlySalary * 0.05; } else if (Type == Administrator) { return monthlySalary * 0.1; } else { return monthlySalary * 0.15; } }
It is not only difficult to read but also resistant to development and prone to errors. We will improve this by creating an abstract class and using the factory method to create an employee with the appropriate role:
public abstract class Employee : Person { public const int Administrator = 0; public const int ContentWriter = 1; public const int Developer = 2; protected Employee(string firstName, string lastName) : base(firstName, lastName) { } public abstract int Type { get; } public abstract double GetMonthlyBonus(double monthlySalary); public static Employee Create(string firstName, string lastName, int type) { return (type) switch { Administrator => new Administrator(firstName, lastName), ContentWriter => new ContentWriter(firstName, lastName), Developer => new Developer(firstName, lastName), _ => throw new ArgumentOutOfRangeException($"Incorrect value: {type}") }; } }
Now we can create a separate subclass for each role:
public class Administrator : Employee { public Administrator(string firstName, string lastName) : base(firstName, lastName) { } public override int Type { get { return Employee.Administrator; } } public override double GetMonthlyBonus(double monthlySalary) { return monthlySalary * 0.1; } }
Adding the next role requires creating a new class and modifying the Create
method.
Each subclass contains an overridden implementation of the GetMonthlyBonus
method that returns the appropriate value for a given role. Thanks to this, we got rid of if
statements. In refactoring terminology, this pattern is called replacing conditional with polymorphism.
Replace Type Code with State/Strategy
Refactoring Large Classes
This code smell refers to a class that has grown too large and is doing too many things. This can make the class difficult to understand, maintain, and test. Additionally, it can also make it harder to reuse the class in other parts of the code.
There are a few common symptoms of a large class:
- The class has a long list of instance variables
- The class has a long list of methods, and many of the methods are long and complex
- The class is responsible for a wide range of tasks and has a lot of responsibilities
- The class is difficult to understand because it is trying to do too many things at once
To fix a large class, you can apply various refactoring patterns such as extract class or extract subclass.
We have already discussed those techniques in the Primitive Obsession section of the article. Remember the Person
class that contained fields like City
, Country
, and PostalCode
? Moving them to the Address
class is nothing more than a class extraction.
Then, let’s recall creating a User
subclass and moving some of the code there that applies only to users, not, for example, employees. Again, we didn’t even know it, and we used an extract subclass pattern!
The use of such refactoring techniques will keep the Single Responsibility Principle intact.
Refactoring Long Methods
Quite obvious and quite easy to fix code smell. Overly long methods are harder to read, modify, and maintain. As a rule of thumb, the method should fit on the screen without scrolling, but ideally, it should be no more than 10 lines long. The method body should only contain actions included in the method name. This also works the other way – a short function is easier to name.
One way to shorten a method is to extract some of the code and move it to a newly created method. Additionally, with the help of an IDE such as Visual Studio, we can do it using the keyboard shortcut Crtl + R + M
, thanks to which the new method will be automatically created with the appropriate return type and the correct list of parameters.
Replace Temp with Query
Speaking of method extraction, one cannot fail to mention the replace temp with query pattern. You can recognize the problem and use this pattern when you see an expression assigned to a local variable:
public double CalculatePriceAfterDiscount(int quantity, int itemPrice) { double orderValue = quantity * itemPrice; if (orderValue > 250) { return orderValue * 0.95; } else { return orderValue; } }
The method is short and very simple, and there are not many conditions, so it doesn’t look bad. But imagine more complicated code, needing to calculate order value multiple times, and maybe different ways of calculating depending on the items – it can be more and more difficult to understand the code.
For this purpose, we should transfer the calculation to a separate method:
public double CalculatePriceAfterDiscount(int quantity, int itemPrice) { if (GetOrderValue(quantity, itemPrice) < 250) { return GetOrderValue(quantity, itemPrice); } else { return GetOrderValue(quantity, itemPrice) * 0.95; } } private static int GetOrderValue(int quantity, int itemPrice) { return quantity * itemPrice; }
The example is very simple, but the more complex and repeated the expression, the easier it will be for us to see the advantage of this solution.
Using a temporary variable, we only do this calculation once, so it should be more conducive to good performance, right? That’s true, but with today’s computers it doesn’t have much of an impact, so it’s better to focus on better readability of the code than a slight and noticeable improvement in performance.
Replace Method with Method Object
We should also mention the replace the method with the method object technique. Since it is quite simple to use, we will only discuss the general concept.
The refactoring pattern involves creating a new class for the method object and moving the method’s code into the new class.
The goal is to divide a lengthy or complex procedure into smaller, more focused portions. This can make the code easier to understand and maintain. It also makes it simpler to unit test the method because we can test it separately from the rest of the code.
Refactoring Long Parameter List
Every time we see a method with a very long list of parameters, a red light should go off right away. Three or four parameters are the maximum number. The only exception is when removing some parameters would introduce unwanted dependencies between classes. In such a case it’s better to turn a blind eye.
How to improve our code?
First of all, it is worth looking at the parameters. If we are passing several values belonging to one class, it would be better to pass the entire object. If not, maybe it’s possible to create a logical class in which we include all the parameters. Adding new classes can cause unwanted project growth, but it is especially worthwhile when we can use it in several places. Thanks to this, we make the code more readable and reduce the number of duplicates in the code.
Of course, you can always look at the body of the method and what precedes it. Maybe we can move some things so that we don’t have to pass some parameters.
Refactoring Data Clumps
A circumstance where the same collection of data is continually sent around together in various portions of the code is referred to as data clumps. As a result, it may be challenging to comprehend and maintain the code since it is unclear why or how the data is being used.
How to fix this?
Again, we can use the extract class refactoring approach. As we saw in the Person
class example, the target is to use a new class to encapsulate the data and the accompanying functionality. By making it apparent what the data stands for and how it is being utilized, we improve the code’s readability and maintainability.
Conclusion
In this article, we discussed various techniques for refactoring bloated C# code and how to identify and resolve bloat. Knowing these methods will significantly impact the quality of our code. We also learned that these techniques can not only rectify code smells but also result in cleaner and easier-to-maintain code.