It 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 - XXXVIII) here.
Let's continue...
You have a clear responsibility for the wrong object
TL;DR: Don't be afraid to create or overload the proper objects.
Finding responsible objects is a tough task.
If we talk to anybody outside the software world, they will tell us where we should place every responsibility.
Software engineers, on the contrary, tend to put behavior in strange places like helpers.
function add(a, b) {
return a + b;
}
// this is natural in many programming languages,
// but unnatural in real life
class GraphicEditor {
constructor() {
this.PI = 3.14;
// We shouldn't define it here
}
pi() {
return this.PI;
// Not this object responsibility
}
drawCircle(radius) {
console.log(`Drawing a circle with radius ${radius}
and circumference ${2 * this.pi() * radius}.`);
}
}
class Integer {
function add(adder) {
return this + adder;
}
}
// This won't compile in many programming languages
// But it is the right place for adding responsibility
class GraphicEditor {
drawCircle(radius) {
console.log(`Drawing a circle with radius ${radius}
and circumference ${2 * Number.pi() * radius}.`);
}
}
// PI's definition is Number's responsibility
This is a semantic smell.
If you put the responsibilities in the proper object, you will surely find them in the same place.
Code Smells are just my opinion.
Photo by Austin Neill on Unsplash
We know an object by what it does, by what services it can provide. That is to say, we know objects by their behaviors.
David West
Software Engineering Great Quotes
You need to model something optional. Have you tried collections?
TL;DR: Collections are fantastic. And Polymorphic.
If you need to model something that might be missing, some fancy languages will provide optional, nullable, and many other wrong solutions dealing with The Billion Dollar Mistake.
Empty collections and non-empty collections are polymorphic.
class Person {
constructor(name, email) {
this.name = name;
this.email = email;
}
email() {
return this.email;
// might be null
}
}
// We cannot use safely person.email()
// We need to check for null explicitly
class Person {
constructor(name, emails) {
this.name = name;
this.emails = emails;
// emails should always be a collection.
// even an empty one
// We can check it here
}
emails() {
return this.emails;
}
// We can mutate the emails since they are not essential
addEmail(email) {
this.emails.push(email);
}
removeEmail(email) {
const index = this.emails.indexOf(email);
if (index !== -1) {
this.emails.splice(index, 1);
}
}
}
// we can iterate the person.emails()
// in a loop without checking for null
You can detect nullable attributes and change them when necessary.
This is a generalization of the null object pattern.
Code Smell 149 - Optional Chaining
Code Smell 19 - Optional Arguments
Null: The Billion Dollar Mistake
Code Smells are just my opinion.
Photo by Levi Jones on Unsplash
To iterate is human, to recurse divine
Peter Deutsch
Which is better, declarative or shorter code?
TL;DR: Be declarative enough but no more.
Last week, a tweet went viral because of a missing formula.
It is the DigiD digital authentication iOS app in the Netherlands.
private static string GetPercentageRounds(double percentage)
{
if (percentage == 0)
return "⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪";
if (percentage > 0.0 && percentage <= 0.1)
return "🔵⚪⚪⚪⚪⚪⚪⚪⚪⚪";
if (percentage > 0.1 && percentage <= 0.2)
return "🔵🔵⚪⚪⚪⚪⚪⚪⚪⚪";
if (percentage > 0.2 && percentage <= 0.3)
return "🔵🔵🔵⚪⚪⚪⚪⚪⚪⚪";
if (percentage > 0.3 && percentage <= 0.4)
return "🔵🔵🔵🔵⚪⚪⚪⚪⚪⚪";
if (percentage > 0.4 && percentage <= 0.5)
return "🔵🔵🔵🔵🔵⚪⚪⚪⚪⚪";
if (percentage > 0.5 && percentage <= 0.6)
return "🔵🔵🔵🔵🔵🔵⚪⚪⚪⚪";
if (percentage > 0.6 && percentage <= 0.7)
return "🔵🔵🔵🔵🔵🔵🔵⚪⚪⚪";
if (percentage > 0.7 && percentage <= 0.8)
return "🔵🔵🔵🔵🔵🔵🔵🔵⚪⚪";
if (percentage > 0.8 && percentage <= 0.9)
return "🔵🔵🔵🔵🔵🔵🔵🔵🔵⚪";
return "🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵";
}
}
}
// Full source
// https://github.com/MinBZK/woo-besluit-broncode-digid-app/blob/master/Source/DigiD.iOS/Services/NFCService.cs
private static string GetPercentageRounds(double percentage)
{
string dots = "🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪";
int blueDots = (int) Math.Truncate (percentage* 10);
int startingPoint = 10-blueDots;
return dots. Substring(startingPoint, 10);
}
This is a semantic smell. In this case, we can count the number of if clauses.
You can read the original Twitter thread to take your own conclusions. There's some serious debate and, of course, several premature optimizators bringing obscure and unneeded solutions with (O) log(n) complexity and stupid benchmarks evidence for a loop that executes only once.
And lots of memes.
As a final conclusion, I asked ChatGPT and was not able to simplify it.
Code Smell 36 - Switch/case/elseif/else/if statements
Code Smell 20 - Premature Optimization
How to Get Rid of Annoying IFs Forever
Code Smells are just my opinion.
There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies.
C. A. R. Hoare
From date should be lower than to date
TL;DR: Intervals are there. Why use plain dates?
The restriction "From date should be lower than to date" means that the starting date of a certain interval should occur before the ending date of the same interval.
The "From date" should be a date that comes earlier in time than the "To date".
This restriction is in place to ensure that the interval being defined makes logical sense and that the dates used to define it are in the correct order.
We all know it. But we miss creating the Interval object.
Would you create a Date as a pair of 3 Integer numbers? Certainly, not.
This is the same.
val from = LocalDate.of(2018, 12, 9)
val to = LocalDate.of(2022, 12, 22)
val elapsed = elapsedDays(from, to)
fun elapsedDays(fromDate: LocalDate, toDate: LocalDate): Long {
return ChronoUnit.DAYS.between(fromDate, toDate)
}
// We need to apply this short function
// Or the inline version many times in our code
// We don't check from Date to be less than toDate
// We can make accounting numbers with a negative number
// We reify the Interval Concept
data class Interval(val fromDate: LocalDate, val toDate: LocalDate) {
init {
if (fromDate >= toDate) {
throw IllegalArgumentException("From date must be before to date")
}
// Of course the Interval must be immutable
// By using the keyword 'data'
}
fun elapsedDays(): Long {
return ChronoUnit.DAYS.between(fromDate, toDate)
}
}
val from = LocalDate.of(2018, 12, 9)
val to = LocalDate.of(2002, 12, 22)
val interval = Interval(from, to) // Invalid
This is a primitive obsession smell.
It is related to how we model things.
If you find software with missing simple validations, it certainly needs reification.
Code Smell 177 - Missing Small Objects
Code Smell 122 - Primitive Obsession
Code Smells are just my opinion.
Photo by Towfiqu barbhuiya on Unsplash
At any particular point in time, the features provided by our programming languages reflect our understanding of software and programming.
R. E. Fairley
Best is to put the expected value last, if conditions you want to write.
TL;DR: In a natural way, write your conditions.
Most programmers write the variable or condition first and the test value second.
In fact, this is the correct order for assertions.
In some languages, this style is used to avoid accidental assignment instead of equality comparison, which can result in a logic error in the code.
if (42 == answerToLifeMeaning) {
//
}
if (answerToLifeMeaning == 42) {
// might be mistaken with answerToLifeMeaning = 42
}
We can check for constant values on the first side of the comparison.
Reliable, direct, and clear be when conditions your writing.
Code Smells are just my opinion.
Any man can make mistakes, but only an idiot persists in his error.
Marcus Cicero
5 more code smells are coming soon…