Part forty one is here!!!
Your code smells because there are likely many instances where it could be edited or improved.
Most of these smells are just hints of something that might be wrong. Therefore, they are not required to be fixed per se… (You should look into it, though.)
You can find all the previous code smells (Part i - XL) here.
Let's continue...
Arrow Code, Nested Conditions, switches, else, and many more
TL;DR: Don't use nested IFs or nested ternaries
Nesting is always a problem with complexity.
We can fix it with polymorphism or early returns
const getUnits = secs => (
secs <= 60 ? 'seconds' :
secs <= 3600 ? 'minutes' :
secs <= 86400 ? 'hours' :
secs <= 2592000 ? 'days' :
secs <= 31536000 ? 'months' :
'years'
)
const getUnits = secs => {
if (secs <= 60) return 'seconds';
if (secs <= 3_600) return 'minutes';
if (secs <= 86_400) return 'hours';
if (secs <= 2_592_000) return 'days';
if (secs <= 31_536_000) return 'months';
return 'years'
}
// More declarative
const getUnits = secs => {
if (secs <= 60) return 'seconds';
if (secs <= 60 * 60) return 'minutes';
if (secs <= 24 * 60 * 60) return 'hours';
if (secs <= 30 * 24 * 60 * 60) return 'days';
if (secs <= 12 * 30 * 24 * 60 * 60) return 'months';
return 'years'
}
Linters can detect this complexity using parsing trees.
We must deal with accidental complexity to improve code readability.
Code Smell 133 - Hardcoded IF Conditions
Code Smells are my opinion.
One of the best things to come out of the home computer revolution could be the general and widespread understanding of how severely limited logic really is.
Frank Herbert
Software Engineering Great Quotes
Constants should be together to find them easily
TL;DR: Don't define too many unrelated constants in the same class. Don't pile up the junk together.
This is a special case of a God Object restricted only to constant definitions.
The repository can be a class, file, or JSON.
public static class GlobalConstants
{
public const int MaxPlayers = 10;
public const string DefaultLanguage = "en-US";
public const double Pi = 3.14159;
}
public static class MaxPlayersConstants
{
public const int MaxPlayers = 10;
}
public static class DefaultLanguageConstants
{
public const string DefaultLanguage = "en-US";
}
public static class MathConstants
{
public const double Pi = 3.14159;
}
We can tell our linters to warn us of too many constants' definitions against a preset threshold.
Finding correct responsibilities is one of our primary tasks when designing software.
Code Smell 29 - Settings / Configs
Code Smell 02 - Constants and Magic Numbers
Photo by Aaron Burden on Unsplash
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.
Martin Fowler
Irrelevant data distract the reader's attention
TL;DR: Don't add unnecessary information to your assertions
Tests should be minimal and follow the SetUp/Exercise/Assert pattern
def test_formula_1_race():
# Setup
racers = [
{"name": "Lewis Hamilton", "team": "Mercedes", "starting_position": 1, "car_color": "Silver", "car_model": "W12"},
{"name": "Max Verstappen", "team": "Red Bull", "starting_position": 2, "car_color": "Red Bull", "car_model": "RB16B"},
{"name": "Sergio Perez", "team": "Red Bull", "starting_position": 3, "car_color": "Red Bull", "car_model": "RB16B"},
{"name": "Lando Norris", "team": "McLaren", "starting_position": 4, "car_color": "Papaya Orange", "car_model": "MCL35M"},
{"name": "Valtteri Bottas", "team": "Mercedes", "starting_position": 5, "car_color": "Silver", "car_model": "W12"},
]
# Exercise
winner = simulate_formula_1_race(racers)
# Test
assert winner == "Lewis Hamilton"
# This is all irrelevant to winner asserting
assert racers[0]["car_color"] == "Silver"
assert racers[1]["car_color"] == "Red Bull"
assert racers[2]["car_color"] == "Red Bull"
assert racers[3]["car_color"] == "Papaya Orange"
assert racers[4]["car_color"] == "Silver"
assert racers[0]["car_model"] == "W12"
assert racers[1]["car_model"] == "RB16B"
assert racers[2]["car_model"] == "RB16B"
assert racers[3]["car_model"] == "MCL35M"
assert racers[4]["car_model"] == "W12"
def test_formula_1_race():
# Setup
racers = [
{"name": "Lewis Hamilton", "starting_position": 1},
{"name": "Max Verstappen", "starting_position": 2},
{"name": "Sergio Perez", "starting_position": 3},
{"name": "Lando Norris", "starting_position": 4},
{"name": "Valtteri Bottas" "starting_position": 5},
]
# Exercise
winner = simulate_formula_1_race(racers)
# Test
assert winner == "Lewis Hamilton"
We can find some patterns in not needed assertions.
Tests should be prose. Always focus on the reader. It might be you a couple of months from now.
Code Smell 76 - Generic Assertions
xUnit Test Patterns: Refactoring Test Code
Photo by Evan Demicoli on Unsplash
Take reasonable steps to test, document, and otherwise draw attention to the assumptions made in every module and routine.
Daniel Read
It is a good idea to assert something has happened in the future
TL;DR: Tests must be in full control and you can't manage time.
I read a Tweet about adding a fixed date to check for the removal of a feature flag (which is another code smell). The test will fail in an unpredictable way preventing releases and breaking CI/CD pipeline. There are also other bad examples we will never reach some date, tests running at midnight, different timezones, etc.
class DateTest {
@Test
void testNoFeatureFlagsAfterFixedDate() {
LocalDate fixedDate = LocalDate.of(2023, 4, 4);
LocalDate currentDate = LocalDate.now();
Assertions.assertTrue(currentDate.isBefore(fixedDate) || !featureFlag.isOn());
}
}
class DateTest {
@Test
void testNoFeatureFlags() {
Assertions.assertFalse(featureFlag.isOn());
}
}
We can check assertions based on time on our tests.
Proceed with caution with tests and dates.
They are often a cause of mistakes.
Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.
Christopher Alexander
You deallocate things in your destructors
TL;DR: Don't use destructors. And don't write functional code there.
A class destructor is a special method that is called when an object is destroyed or goes out of scope.
In the past, we had no garbage collectors and destructors were responsible for cleaning up any resources that the object has acquired during its lifetime, such as closing open files or releasing memory allocated on the heap.
Nowadays, object destruction is automatic in most modern programming languages.
class File {
public:
File(const std::string& filename) {
file_ = fopen(filename.c_str(), "r");
}
~File() {
if (file_) {
fclose(file_);
}
}
private:
FILE* file_;
};
class File {
public:
File() : file_(nullptr) {}
bool Open(const std::string& filename) {
if (file_) {
fclose(file_);
}
file_ = fopen(filename.c_str(), "r");
return (file_ != nullptr);
}
bool IsOpen() const {
return (file_ != nullptr);
}
void Close() {
if (file_) {
fclose(file_);
file_ = nullptr;
}
}
~File() {
// Instead of closing the file we throw an exception
// If it is open (which is an invalid scenario)
if (file_) {
throw std::logic_error("File is still open in destructor");
}
}
private:
FILE* file_;
};
Linters can warn us when we write code in destructors
In very critical low-level code we cannot afford a garbage collector.
Exceptions are very few.
In other cases writing code in destructors is a symptom of premature optimization.
Writting code in destructors is a sign of sloppiness and laziness.
We need to understand the life cycle of our objects and manage the events accurately.
Code Smell 142 - Queries in Constructors
Photo by Crawford Jolly on Unsplash
The most likely way for the world to be destroyed, most experts agree, is by accident. That's where we come in; we're computer professionals. We cause accidents.
Nathaniel S. Borenstein
5 more code smells are coming soon…
My new book about clean code is available for pre-order.
You will find several recipes like this one with a higher level of detail