Developing as a Developer: Code Smells - Bloaters

In this installment of the Developing as a Developer series I will be exploring the code smells belonging to the category known as Bloaters, which Mäntylä and Lassenius (2006) define as "something that has grown so large that it cannot be effectively handled."
Links to other posts in this series:
Long Function
This code smell arises when a method or function is simply too large. The basis for this smell is that the longer a function is, the more difficult it is to reason about. Since modern languages have mostly eliminated overhead costs associated with in-process calls, there is significant value in breaking a large function up into smaller functions. Furthermore, the key to easily understandable small functions is deliberate and descriptive naming, which serves to reduce the burden placed on the reader caused by switching context to see what the function does. A good rule of thumb is to write a function whenever you feel the need to comment something, such that the function contains the code that you wanted to comment and is named after the intention (Fowler, 2018, p.73).
Refactoring Solutions
Extract Function
This is the first strategy to reach for when trying to shorten a function. The goal is to take a fragment of code that shares a similar purpose, and then extract it into its own aptly named function.
For example, let's say you wanted to make a pizza, which involves 3 main steps:
Make the dough (regular or gluten-free)
Assemble the pizza
Bake the pizza
We could combine it all into one function as follows (apologies if I missed some steps if there are any pizza experts here 🥲):
def make_pizza(dough_variant):
# Make the dough
activate_yeast()
add_olive_oil()
if dough_variant == "regular":
add_flour()
if dough_variant == "gluten-free":
add_almond_flour()
knead_dough()
rise_dough()
toss_dough()
assemble_pizza()
bake_pizza()
We could extract all the steps for making the dough into its own function that we call within the make_pizza(variant) method:
def make_dough(variant):
activate_yeast()
add_olive_oil()
if variant == "regular":
add_flour()
if variant == "gluten-free":
add_almond_flour()
knead_dough()
rise_dough()
toss_dough()
def make_pizza(dough_variant):
make_dough(dough_variant)
assemble_pizza()
bake_pizza()
Encapsulating the irrelevant implementation details makes it much easier to reason about what the make_pizza(variant) function is doing. We also now have the added benefit of reusability, such that the make_dough(variant) function can be called elsewhere if we were to extend the functionality (e.g., different types of pizza that use the same dough). The caveat with this technique is that the names must be clear and descriptive to minimize the effort spent discerning the purpose of a code fragment (Fowler, 2018, p.107).
In some cases there may be many parameters and temporary variables that make it difficult to extract a function as the result would require forwarding all that context to the new method. Fowler provides a number of options for dealing with these based on the specific obstacle.
Replace Temp with Query
Let's expand the first example to have make_pizza use a temporary variable bake_temp that it passes to the bake_pizza method based on the desired "crispiness" (another new parameter) of the pizza:
def make_pizza(dough_variant, crispiness):
make_dough(dough_variant)
if crispiness == "well-done":
bake_temp = 500
if crispiness == "regular":
bake_temp = 450
assemble_pizza()
bake_pizza(bake_temp)
This is a problem because now anytime we want to extract something that requires that context, we have to duplicate the logic. Instead, we turn it into a query (a simple method) that contains the logic and returns the value we need.
def bake_temp_for(crispiness):
if crispiness == "well-done":
return 500
if crispiness == "regular":
return 450
def make_pizza(dough_variant, crispiness):
make_dough(dough_variant)
assemble_pizza()
bake_pizza(bake_temp_for(crispiness))
Introduce Parameter Object
Fowler introduces the idea of a data clump as a groups of items that consistently appear together (Fowler, 2018, p.140). In the pizza example, as the make_pizza method grows more realistic (in the sense that it accounts for all the factors that actually matter for making a pizza), we end up having to pass more and more data to our extracted helper functions. We actually can see this happening as we extend the functionality to account for things like crispiness. What would happen if we were to introduce a few more factors, such as size, sauce, and toppings? We would definitely need to consider the size when adding sauce and toppings because it determines the amount used. Similarly, the size determines the quantity of ingredients that go into making our dough, and how long the pizza bakes for.
def make_pizza(dough_variant, crispiness, size, sauce, toppings):
make_dough(dough_variant, size)
assemble_pizza(size, sauce, toppings)
bake_pizza(bake_temp_for(crispiness), size)
You can already see how this is becoming tedious. Instead, what if we were to bundle all the features into a single order object:
class PizzaOrder:
def __init__(self, dough_variant, crispiness, size, sauce, toppings):
self._dough_variant = dough_variant
self._crispiness = crispiness
# ... You get the point
def make_pizza(order):
make_dough(order)
assemble_pizza(order)
bake_pizza(order)
We now have a much more concise make_pizza function that only takes in a single argument, from which we can extract the properties we need in each helper function. Fowler argues for passing the whole record (what he calls "Preserve the Whole Object") rather than deriving select values for a few reasons (Fowler, 2018, p.318):
Protects against future changes. For example, what happens when we need to extract more values, such a
cheesevariable from thePizzaOrderIt reduces the size of the parameter list, which makes the function more comprehensible.
If there are many functions called with the parts, that logic can be moved to the object so it can be invoked by the helper functions.
Replace Function with Command
If none of the above strategies successfully resolve the interdependent local variables and parameters, Fowler recommends switching to what he calls the heavy artillery: Replace Function with Command (Fowler, 2018, p.73).
In this strategy, you convert the entire function into an object, where the temporary variables become fields and the sub-steps become methods that share state. Consider the following PizzaMaker class to replace the make_pizza function:
class PizzaMaker:
def __init__(self, order):
self.order = order
def make(self):
self._make_dough()
self._assemble()
self._bake()
def _make_dough(self):
# Steps to make the dough
def _assemble(self):
# Steps to assemble the pizza
def _bake(self):
# Steps to bake the pizza
While this is not a perfect example because it's particularly long or complicated, hopefully you can see the goal of breaking down the function into methods and fields contained within a class. We can leverage the properties provided by classes such as shared state to create methods that manipulate parameters, eliminating the need to pass around variables from the main function to helpers.
The strategies above aim to resolve excessively large functions by extracting code fragments into logical groupings and clearing obstacles that prevent us from doing that cleanly. The next few strategies provide ways to remove conditionals and loops, which are often good candidates for extraction.
Decompose Conditional
The essence of this technique is to delegate the specifics of conditional logic into its own clearly named function that makes the calling code easier to read and reason about. Imagine our hypothetical pizza shop is running a promotion where a large pizza is half off from 2-3 pm (a pizza happy hour, if you will).
def calculate_pizza_cost(order):
if (2pm ≤ datetime.now() ≤ 3pm): # Pseudocode
return order.pizza.cost * 0.5
else:
return order.pizza.cost
Instead, we can delegate this logic to a new clearly named function that gives us tells us which rate we should use. We can even decompose the legs so that the evaluation reads more clearly:
def is_pizza_happy_hour():
if (2pm ≤ datetime.now() ≤ 3pm): # Pseudocode
return True
else:
return False
def happy_hour_cost(order):
return order.pizza.cost * 0.5
def regular_cost(order)
return order.pizza.cost
def calculate_pizza_cost(order):
if is_pizza_happy_hour():
return happy_hour_cost(order)
else:
return regular_cost(order
This allows the reader to quickly grasp the purpose of the calculate_pizza_cost function without getting caught up trying to trace the branching logic of the conditional.
Replace Conditional with Polymorphism
This strategy is actually quite similar to the Strategy Pattern (which I previously wrote about). The goal is to eliminate large conditional branches by replacing them with classes and polymorphism. Consider the logic we use to assign a dough, which involves adding a different flour for each distinct type:
def make_dough(dough_variant):
if variant == "regular":
add_flour()
if variant == "gluten-free":
add_almond_flour()
# New variant: whole wheat
if variant == "whole wheat":
add_whole_wheat_flour()
Instead of repeating the conditional or decomposing it, we instead convert it to it's own type, letting each concrete implementation handle the implementation details:
class Dough:
def add_flour(self):
# Implemented by specific types
class RegularDough(Dough):
def add_flour(self):
add_flour()
class GlutenFreeDough(Dough):
def add_flour(self):
add_almond_flour()
class WholeWheatDough(Dough):
def add_flour(self):
add_whole_wheat_flour()
def make_dough(dough):
activate_yeast()
add_olive_oil()
dough.add_flour()
# ... rest of steps
Now instead of adding a conditional branch whenever we need to add a new variant, we can simply add a child to the Dough class, then define the add_flour method.
Split Loop
Fowler posits that loops should be extracted into their own methods, and further separated into distinct functions (Fowler, 2018, p.227). He gives the example of a loop that updates two variables on each iteration (stepping away from the pizza example because it doesn't make much sense here):
people = [person_a, person_b, person_c, ...]
average_age = 0
total_salary = 0
for person in people:
average_age += person.age
total_salary += person.salary
average_age = average_age / len(people)
# Applying the Split Loop refactor, this would become:
total_salary = 0
for person in people:
total_salary += person.salary
average_age = 0
for person in people:
average_age += person.age
average_age = average_age / len(people)
While it does slightly increase the amount of code written and force us to execute the loop twice, it removes the ambiguity of what the loop is actually doing, which allows us to clearly describe each one and place it into it's own method that can replace the code in the larger function. Fowler emphasizes the importance of separating refactoring from optimization, noting that we can easily recombine the loops if it turns out to be a bottleneck in the future (Fowler, 2018, p.228).
def total_salary():
total_salary = 0
for person in people:
total_salary += person.salary
return total_salary
def average_age():
average_age = 0
for person in people:
average_age += person.age
return average_age
Long Parameter List
First, let's understand the purpose of a parameter list:
"The parameter list to a function should summarize the points of variability of the function, indicating the primary ways in which that function may behave differently" (Fowler, 2018, p.324).
This smell appears when a function requires a large amount of data and requires an extensive parameter list to be passed in. In such cases, it becomes challenging to reason about the key ways a function might differ.
Refactoring Solutions
Replace Parameter with Query
Reach for this strategy when an argument passed into the function can be easily derived by the function itself. The idea is to shift the responsibility of determining the value from the calling code to the function itself. Fowler notes that this technique should not be used if removing the parameter adds an unwanted dependency to the function (i.e., an element that the function should be ignorant of). The ideal case is when the value you want to remove from the function arguments is retrieved by querying another parameter in the list.
Consider the following Order class
class Order:
def __init__(self, quantity, item_price):
self._quantity = quantity
self._item_price = item_price
self._discount_level = 2 if quantity > 100 else 1
def final_price(self):
base_price = self._quantity * self._item_price
return discounted_price(base_price, self._discount_level)
def discounted_price(base_price, discount_level):
if discount_level == 1:
return base_price * 0.95
elif discount_level == 2:
return base_price * 0.9
Rather than pass in self._discount_level to the method, we can remove it from the parameter list and simply derive it in the function:
def discounted_price(base_price):
if self._discount_level == 1:
return base_price * 0.95
elif self._discount_level == 2:
return base_price * 0.9
Preserve Whole Object
In the event where you are passing several individual properties of an object as arguments, consider passing the whole entity instead, then extracting only what you need. Take the example below, where we pass in each parameter from our order individually:
def make_pizza(toppings, sauce, dough_variant, crispiness, cheese):
make_dough(dough_variant)
# ... you get the point by now
order = Order(
toppings = ['pepperoni', 'bell peppers'],
sauce = 'regular',
dough_variant = 'regular',
crispiness = 'well-done',
cheese = 'fresh mozzarella'
)
make_pizza(order.toppings, order.sauce, order.dough_variant, order.crispiness, order.cheese)
Alternatively, we simply pass in the whole object, then extract what we need:
def make_pizza(order):
make_dough(order.dough_variant)
# ... you get the point by now
order = Order(
toppings = ['pepperoni', 'bell peppers'],
sauce = 'regular',
dough_variant = 'regular',
crispiness = 'well-done',
cheese = 'fresh mozzarella'
)
make_pizza(order)
Introduce Parameter Object
If this looks familiar... that's because it is. It is in fact the same exact strategy recommended for reducing long functions, and is what would enable us to perform the refactor shown above.
Remove Flag Argument
A flag argument refers to an argument the caller passes to indicate which behavior to execute. Fowler avoids flag arguments as they make it harder to understand what function calls are available and how to call them (Fowler, 2018, p.315). To illustrate the problem, consider the the following line of code: make_pizza(order, True)
What is True doing here? There's no way to know what it's doing without opening the make_pizza and reading through the body– it silently switches the behavior of the function but tells the reader nothing.
Consider the following example where we use an is_rush flag:
def make_pizza(order, is_rush):
make_dough(order)
assemble_pizza(order)
if is_rush:
express_bake(order)
else:
standard_bake(order)
In this case, the flag is more descriptive, but the reader still has to open the make_pizza method and read through it to understand what behavior change is actually taking place. Instead, we remove the flag argument, replacing it with explicit, intention-revealing functions:
def make_rush_pizza(order):
make_dough(order)
assemble_pizza(order)
express_bake(order)
def make_regular_pizza(order):
make_dough(order)
assemble_pizza(order)
standard_bake(order)
And if you wanted to eliminate the duplicate body:
def make_rush_pizza(order):
_make_pizza(order, express_bake)
def make_regular_pizza(order):
_make_pizza(order, standard_bake)
def _make_pizza(order, bake_step):
make_dough(order)
assemble_pizza(order)
bake_step(order)
Note that bake_step is not a flag being decoded, but a function that is passed from the public entry points (make_rush_pizza and make_regular_pizza)
While there is a time and place for flag arguments, such as avoiding writing explicit functions for every combination of flag values, they serve as an indicator that a function is doing too much.
Combine Functions into Class
This technique works well when multiple functions share several parameter values. The idea is to create a class containing the common fields. Consider the following example where multiple steps involved in making the pizza share some fields:
def make_dough(size, dietary_restrictions, dough_variant):
# Logic for making the dough
def assemble_pizza(size, dietary_restrictions, toppings, sauce, cheese):
# Logic for assembling the pizza
def bake_pizza(size, dietary_restrictions, crispiness):
# Logic for baking the pizza
Noticing that all three of these functions share two of the same parameters, we create a PizzaMaker class that encapsulates the common data (i.e., make the shared arguments fields):
class PizzaMaker:
def __init__(self, size, dietary_restrictions):
self.size = size
self.dietary_restrictions = dietary_restrictions
def make_dough(self, dough_variant):
# uses self.size, self.dietary_restrictions, and dough_variant
def assemble_pizza(self, toppings, sauce, cheese):
# uses self.size, self.dietary_restrictions, and the rest
def bake_pizza(self, crispiness):
# uses self.size, self.dietary_restrictions, and crispiness
Now observe how this simplifies the client code:
# Before
make_dough(size, dietary_restrictions, dough_variant)
assemble_pizza(size, dietary_restrictions, toppings, sauce, cheese)
bake_pizza(size, dietary_restrictions, crispiness)
# After
maker = PizzaMaker(size, dietary_restrictions)
maker.make_dough(dough_variant)
maker.assemble_pizza(toppings, sauce, cheese)
maker.bake_pizza(crispiness)
This solution falls into a Long Parameter List fix since the shared parameters were the bloat, and moving them into fields shrinks all the signatures simultaneously. It also sets us up for later refactoring, since any helper that is defined inside the class can freely use self.size and self.dietary_restrictions. If you were paying attention, you might point out that we could further refactor this by combining all shared fields into a single Order object that is initialized in the PizzaMaker class, from which the helper functions can derive what they need.
Additionally, while this resembles the solution to Replace Function with Command from the previous section, note that they are solutions answer to solve different problems. In the original example, we created a class to break down an excessively large function into an object so that the internal methods could share state. In the present example, we used a class to encapsulate shared data and reduce the size of the parameter lists and simplify the calling code.
Large Class
Classes that attempt to do too much, and often contain too many fields
Refactoring Solutions
Extract Class
This technique involves bundling related variables into their own components. To demonstrate, imagine our Order object from before contained information about both the pizza and the individual who placed the order.
class Order:
def __init__(toppings, dough_variant, sauce, ..., customer_name, customer_phone_number, customer_address):
self.toppings = toppings
self.customer_name = customer_name
# ...
At this point we can take the customer information and bundle it into a Customer class, then pass the entire object into the Order class:
class Customer:
def __init__(name, phone_number, address):
self.name = name
self.phone_number = phone_number
self.address = address
class Order:
def __init__(toppings, dough_variant, sauce, ..., customer)
self.toppings = toppings
self.customer = customer
Extract Superclass
This refactoring strategy leverages the power of inheritance to consolidate similarities among multiple classes into a superclass. Imagine you are creating creatures for a video game, such as "werewolf" and "undead"
class Werewolf:
def __init__(self, name, hp, mp, strength):
self.name = name
self.hp = hp
self.mp = mp
self.strength = strength
def beast_claw(self):
return f"{self.name} attacked for {self.strength} damage"
class Undead:
def __init__(self, name, hp, mp, intelligence):
self.name = name
self.hp = hp
self.mp = mp
self.strength = strength
def rot_breath(self):
return f"{self.name} attacked for {self.intelligence} damage"
We can combine these into a common superclass which extracts the fields and methods that are shared and leaves what differs to the subclasses. To do this, we apply what Fowler (2018) describes as Pull Up Field (p.353) and Pull Up Method (p.350). In this example, the methods used to attack have different names and bodies, but represent the same intent. With an additional helper function to get attack_power and a small refactor we can also move a generic attack function to the superclass using what Fowler labels the Form Template Method (Fowler, 2018, p.351):
class Enemy:
def __init__(self, name, hp, mp):
self.name = name
self.hp = hp
self.mp = mp
def attack(self):
return f"{self.name} attacked for {self.attack_power()} damage"
def attack_power(self):
raise NotImplementedError # each creature supplies its own
class Werewolf(Enemy):
def __init__(self, name, hp, mp, strength):
super().__init__(name, hp, mp)
self.strength = self.strength
def attack_power(self):
return self.strength
class Undead(Enemy):
def __init__(self, name, hp, mp, intelligence):
super().__init__(name, hp, mp)
self.intelligence = intelligence
def attack_power(self):
return self.intelligence
Data Clumps
Data clumps describe entities that are frequently found together, such as a group of fields in multiple classes or parameters in several methods.
Refactoring Solutions
Data clumps can be dealt with by employing three refactors we have already seen before and go hand-in-hand. Once you've identified the common fields, group them into an object (Extract Class) to provide a layer of abstraction and separate concerns. This makes it easier to maintain since the data that changes together or are dependent on each other are now found together. If the data clump occurs in a method, you can pass the newly created class as a parameter (Introduce Parameter Object), reducing the bloated signature and allowing you to extract what you need in the function itself (Preserve Whole Object).
Primitive Obsession
Primitives are the most basic, built-in data types offered by a language, such as strings, arrays, integers, and booleans. The essence of this smell is an overuse of primitive types as opposed to developing your own simple classes to enforce special behaviors.
Refactoring Solutions
Replace Primitive with Object
Fowler (2018) comments that any time he wants to do more than simply printing an object, he creates a new class (p.174). This provides the developer with a clear place to add behavior specific to the data. Consider an integer crispiness level parameter in a pizza baking function.
def bake_pizza(order, crispiness):
if crispiness < 1 or crispiness > 5: # validation here
raise ValueError("crispiness must be 1-5")
minutes = 8 + crispiness * 2 # conversion logic here
...
def print_receipt(order, crispiness):
labels = {1: "Soft", 3: "Golden", 5: "Extra Crispy"} # formatting here
print(f"Crispiness: {labels.get(crispiness)}")
While we can introduce guards and raise exceptions to enforce the behavior we want (integer between 1 and 5, bake time proportional to crispiness), the integer cannot validate itself, name itself, or tell you its bake time. To clean up this function, we can gather up all that information into a Crispiness object that implements all of that behavior:
class Crispiness:
def __init__(self, level: dict):
if not 1 <= level <= 5:
raise ValueError("crispiness must be 1-5")
self.level = level
def bake_minutes(self):
return 8 + self.level * 2
def label(self):
return {
1: "Soft",
3: "Golden",
5: "Extra Crispy"
}.get(self.level, "Medium") # Levels 2 and 4 default to "Medium"
# Revised function passing in whole object
def bake_pizza(order, crispiness):
bake_for(crispiness.bake_minutes())
def print_receipt(order, crispiness):
print(f"Crispiness: {crispiness.label()}")
Replace Type Code with Subclasses
A type code is a primitive field that determines what kind of thing an object is. Extending the game creatures from before, we might initialize the Enemy class with a creature_type type code:
class Enemy:
def __init__(self, name, hp, mp, creature_type):
self.name = name
self.hp = hp
self.mp = mp
self.creature_type = creature_type # "werewolf" or "undead" — the type code
def attack(self):
if self.creature_type == "werewolf":
return f"{self.name} slashes with claws"
elif self.creature_type == "undead":
return f"{self.name} breathes rot"
def weakness(self):
if self.creature_type == "werewolf":
return "silver"
elif self.creature_type == "undead":
return "fire"
With this approach, we end up having to recreate the if-else branching logic for every method. This means an addition of a new enemy would require updating every single conditional (Fowler calls this smell "Repeated Switches"). The result is much simpler methods that leave the concrete implementation details to the subclasses to prevent bloated methods and duplicate code:
class Enemy:
def __init__(self, name, hp, mp):
self.name = name
self.hp = hp
self.mp = mp
class Werewolf(Enemy):
def attack(self):
return f"{self.name} slashes with claws"
def weakness(self):
return "silver"
class Undead(Enemy):
def attack(self):
return f"{self.name} breathes rot"
def weakness(self):
return "fire"
Takeaways
Something I noticed is that the book itself is written following Fowler's own refactoring principles. The names are clear and the refactoring strategies are designed to solve singular problems. He even breaks down smaller operations into their own strategies, such as "Encapsulate Variable" (p. 132) and "Rename Field" (p.244). The sheer amount of information contained in Fowler's book is overwhelming, but I do appreciate his consistency. While it's difficult at first when trying to digest the countless smells and refactors, the way he builds up each strategy help quickly create a mental model and solidify patterns.
References:
Fowler, M. (2018). Refactoring: Improving the design of existing code (2nd ed.). Addison-Wesley Professional
Mäntylä, M.V., Lassenius, C. Subjective evaluation of software evolvability using code smells: An empirical study. Empir Software Eng 11, 395–431 (2006). https://doi.org/10.1007/s10664-006-9002-8



