Write Code Like a Senior: 5 Must-Know Tips for Crafting Code
In this article, we are talking about 5 simple tips you can start using this moment to improve your code quality.
No matter what programming language you use or how many years of experience you have, you can use these tips in your daily work.
So let's start.
1) Nesting and early returns
For sure, everyone encountered some heavy nested code in their projects.
Nested code refers to code structures where one block of code contains another. This often results in many levels of indentation. While nested code isn't bad, excessive nesting can lead to several issues. These issues can make code harder to, maintain, and debug.
Nesting can be problematic because:
- Complexity and readability - it's very hard to read nested code
- Maintenance - comes as a side-effect of the first point, nested code is harder to maintain
- Debugging - it's harder to debug nested code
- Scoping issues - often variables overlap and cause scoping issues
- Testing - it's harder to write tests for heavy nested code
Take a look at this code snippet:
if (user && user.isAuthenticated) {
// ...20 lines of code
if (timezone) {
// ...10 lines of code
if (isQualified) {
// ...50 lines of code
}
}
}
what should be done instead?
const userAuthenticated = user && user.isAuthenticated;
if (!userAuthenticated) {
return;
}
// ...20 lines of code
const timezone = ...
if (!timezone) {
return;
}
// ...10 lines of code
const isQualified = ...
if (!isQualified) {
return;
}
// ...50 lines of code
You see how much better this is. There are no indentations and everything is much easier to read.
Another thing related to nesting is the early return concept.
Early return is a programming concept where a function exits and returns a value before completing its code. This is often used to handle special cases or error conditions.
Let's take a look at this example:
function getDailyRewards(user){
if(user.isAuthenticated){
// ...50 lines of code
}
}
This if condition wraps ~50 lines of code. Ugly as hell. Let's rewrite it:
funcion getDailyRewards(user) {
if (!user.isAuthenticated) {
return;
}
// ...50 lines of code
}
Much better.
Instead of wrapping the whole code logic with an if condition we exit early if a user is not authenticated. The rest of the code is below the guard clause and it's much easier to read it.
2) Useless if/else blocks
This one is specific to beginners. I like to bug my students on exercises when they write useless if/else blocks in the code.
Here is one classic example:
function isEven(number) {
if (number % 2 === 0) {
return true;
} else {
return false;
}
}
When I see code like this, I ask them if they can write it shorter, more elegant. Then, some of them do something like this:
function isEven(number) {
if (number % 2 === 0) {
return true;
}
return false;
}
Then I ask them if they can write those 4 lines in 1 line. Then they do something like this:
function isEven(number) {
return number % 2 === 0 ? true : false;
}
Again, I ask them if they can do it without if/else or ternary operator, and then finally we get something like this:
function isEven(number) {
return number % 2 === 0;
}
Here is one more example, similar to first case.
function getScheduleKey() {
const hasSchedule = this.hasTeleVisitScheduled();
if (hasSchedule) {
return this.schedule.key;
} else {
return this.schedule.cancelSchedule.key;
}
}
If you look closely, there is no need to write else block here. The function is ending and there is a return anyway.
So we can fix it like this:
function getScheduleKey() {
const hasSchedule = this.hasTeleVisitScheduled();
if (hasSchedule) {
return this.schedule.key;
}
return this.schedule.cancelSchedule.key;
}
The moral of the story, avoid useless if/else blocks.
3) Hardcoded strings and numbers
Hardcoding strings and numbers embed values into your code. Instead of using variables or constants to represent them.
There are several reasons why hardcoding values are generally considered bad practice:
- Lack of flexibility and maintainability: If you hardcode values throughout your codebase, making changes to those values requires finding and modifying each occurrence. This means if you hardcoded a string 100 times in the code and after 2 months you need to change that string, you will need to change it again in 100 places.
- Reusability: Hardcoding values make it difficult to reuse the same code in different contexts. If you want to use the same logic with different values, you would need to duplicate the code. Then you can change the values, and this is leading to code duplication.
- Readability: Code with hardcoded values can become harder to read and understand. Especially for other developers who might not be familiar with the context. Meaningful variable names make the purpose of the value more clear.
- Debugging and error detection: When errors occur, it's easier to spot mistakes if you're using named variables or constants.
- Scalability: As your project grows, maintaining hardcoded values becomes challenging. Changing a single value across the codebase can become a tedious and error-prone task.
- Localization and internationalization: If your code needs to support many languages or locales, hard-coded values can make it difficult to translate.
To conclude, it's bad to have hardcoded strings and numbers in your code.
Now, let's take a look at this beautiful example that inspired me to write this article. It was much longer in original form, but here is shorter version with fewer if conditions:
if (fitnessTrainingSchedule.name == "Training 03 Schedule") {
checkWindowForTrainingSchedule(10, 16, "Milestone3Schedule", "Training 03 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 04 Schedule") {
checkWindowForTrainingSchedule(24, 30, "Milestone4Schedule", "Training 04 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 05 Schedule") {
checkWindowForTrainingSchedule(52, 58, "Milestone5Schedule", "Training 05 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 06 Schedule") {
checkWindowForTrainingSchedule(80, 86, "Milestone6Schedule", "Training 06 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 07 Schedule") {
checkWindowForTrainingSchedule(108, 114, "Milestone7Schedule", "Training 07 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 08 Schedule") {
checkWindowForTrainingSchedule(136, 142, "Milestone8Schedule", "Training 08 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 09 Schedule") {
checkWindowForTrainingSchedule(164, 170, "Milestone9Schedule", "Training 09 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 10 Schedule") {
checkWindowForTrainingSchedule(216, 230, "Milestone10Schedule", "Training 10 Schedule");
}
On the first look, you can see a bunch of numbers and strings that are similar but you have no clue what is this code doing. All you know it is doing something with fitness training schedules. To understand this code, you need to take a look in other scripts.
Let's see the refactored version:
const TRAINING_SCHEDULE_03 = {
name: "Training 03 Schedule",
milestone: "Milestone3Schedule",
startDaysInterval: 10,
endDaysInterval: 16
};
const TRAINING_SCHEDULE_04 = {
name: "Training 04 Schedule",
milestone: "Milestone4Schedule",
startDaysInterval: 24,
endDaysInterval: 30
};
const TRAINING_SCHEDULE_05 = {
name: "Training 05 Schedule",
milestone: "Milestone5Schedule",
startDaysInterval: 52,
endDaysInterval: 58
};
const TRAINING_SCHEDULE_06 = {
name: "Training 06 Schedule",
milestone: "Milestone6Schedule",
startDaysInterval: 80,
endDaysInterval: 86
};
const TRAINING_SCHEDULE_07 = {
name: "Training 07 Schedule",
milestone: "Milestone7Schedule",
startDaysInterval: 108,
endDaysInterval: 114
};
const TRAINING_SCHEDULE_08 = {
name: "Training 08 Schedule",
milestone: "Milestone8Schedule",
startDaysInterval: 136,
endDaysInterval: 142
};
const TRAINING_SCHEDULE_09 = {
name: "Training 09 Schedule",
milestone: "Milestone9Schedule",
startDaysInterval: 164,
endDaysInterval: 170
};
const TRAINING_SCHEDULE_10 = {
name: "Training 10 Schedule",
milestone: "Milestone10Schedule",
startDaysInterval: 216,
endDaysInterval: 230
};
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_03.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_03.startDaysInterval,
TRAINING_SCHEDULE_03.endDaysInterval,
TRAINING_SCHEDULE_03.milestone,
TRAINING_SCHEDULE_03.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_04.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_04.startDaysInterval,
TRAINING_SCHEDULE_04.endDaysInterval,
TRAINING_SCHEDULE_04.milestone,
TRAINING_SCHEDULE_04.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_05.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_05.startDaysInterval,
TRAINING_SCHEDULE_05.endDaysInterval,
TRAINING_SCHEDULE_05.milestone,
TRAINING_SCHEDULE_05.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_06.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_06.startDaysInterval,
TRAINING_SCHEDULE_06.endDaysInterval,
TRAINING_SCHEDULE_06.milestone,
TRAINING_SCHEDULE_06.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_07.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_07.startDaysInterval,
TRAINING_SCHEDULE_07.endDaysInterval,
TRAINING_SCHEDULE_07.milestone,
TRAINING_SCHEDULE_07.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_08.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_08.startDaysInterval,
TRAINING_SCHEDULE_08.endDaysInterval,
TRAINING_SCHEDULE_08.milestone,
TRAINING_SCHEDULE_08.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_09.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_09.startDaysInterval,
TRAINING_SCHEDULE_09.endDaysInterval,
TRAINING_SCHEDULE_09.milestone,
TRAINING_SCHEDULE_09.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_10.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_10.startDaysInterval,
TRAINING_SCHEDULE_10.endDaysInterval,
TRAINING_SCHEDULE_10.milestone,
TRAINING_SCHEDULE_10.name
);
}
This code is definitely longer but it's more readable and understandable. You can see what this code is doing and what is the meaning of the parameters.
Of course, it's far from perfect code. It contains a lot of repetitive code which we are gonna fix next.
4) Repetitive code
Code repetition is also known as code duplication. It refers to having the same or similar blocks of code appear in many places within a project. Code repetition is generally considered bad in programming for several reasons:
- Maintenance complexity: When the same code appears in many places, any changes or updates to that code need to be made in all those places. This increases the risk of introducing errors. It also makes maintenance more difficult and time-consuming.
- Bug propagation: If there is a bug in the repeated code, it needs to be fixed in every instance. Missing even one instance could lead to inconsistent behavior or unexpected issues.
- Readability and understanding: Repeated code can make the codebase harder to read and understand. Especially for other developers who might need to work on or maintain the code. It obscures the logic and intent of the project.
- Code bloat: Repeated code contributes to code bloat, making the project larger and slower. This can affect performance and efficiency.
- Code reviews and collaboration: During code reviews, duplicated code can be flagged as an issue. This leads to discussions about code organization and design choices. It can also create conflicts in version control systems. Especially, when many developers are working on similar code.
The code example we refactored before has a huge problem and that's code repetition. It violates the DRY principle which says "don't repeat yourself".
Let's fix that:
const TRAINING_SCHEDULES = [
{
name: "Training 03 Schedule",
milestone: "Milestone3Schedule",
startDaysInterval: 10,
endDaysInterval: 16
},
{
name: "Training 04 Schedule",
milestone: "Milestone4Schedule",
startDaysInterval: 24,
endDaysInterval: 30
},
{
name: "Training 05 Schedule",
milestone: "Milestone5Schedule",
startDaysInterval: 52,
endDaysInterval: 58
},
{
name: "Training 06 Schedule",
milestone: "Milestone6Schedule",
startDaysInterval: 80,
endDaysInterval: 86
},
{
name: "Training 07 Schedule",
milestone: "Milestone7Schedule",
startDaysInterval: 108,
endDaysInterval: 114
},
{
name: "Training 08 Schedule",
milestone: "Milestone8Schedule",
startDaysInterval: 136,
endDaysInterval: 142
},
{
name: "Training 09 Schedule",
milestone: "Milestone9Schedule",
startDaysInterval: 164,
endDaysInterval: 170
},
{
name: "Training 10 Schedule",
milestone: "Milestone10Schedule",
startDaysInterval: 216,
endDaysInterval: 230
}
];
const targetedTrainingSchedule = TRAINING_SCHEDULES.find(
(trainingSchedule) => trainingSchedule.name === fitnessTrainingSchedule.name
);
if (!targetedTrainingSchedule) {
return;
}
checkWindowForTrainingSchedule(
targetedTrainingSchedule.startDaysInterval,
targetedTrainingSchedule.endDaysInterval,
targetedTrainingSchedule.milestone,
targetedTrainingSchedule.name
);
As you can see we moved all objects in an array. Then, instead of repeating if conditions for every possible fitness training schedule, we find the one we are looking for. Then we pass the params from the targeted training schedule into function.
This looks much nicer than before.
5) Function arguments mess
Function arguments, also known as parameters, are values that you provide to a function when you call it. These arguments provide the necessary data for the function to perform its task or calculations.
If you are not careful with function arguments, you can create a mess in the code. I once encountered a function that looked like this:
function createUser(
username,
email,
password,
firstName,
lastName,
birthdate,
gender,
phoneNumber,
profilePicture,
registerPurpose,
isActive,
isAdmin,
isVerified,
registrationDate,
status,
timezone,
theme,
accountType,
subscriptionPlan,
preferredLanguage,
expiryDate,
lastPasswordUpdate,
lastLogin,
notificationSettings,
customFields
)
When a function has a large number of parameters, it becomes harder to understand, read, and maintain. It can be challenging to keep track of what each parameter does and how they interact with each other.
A function with many arguments can make the code harder to read and understand. Especially for developers who are new to the codebase.
Also, testing functions with many parameters becomes more complex. Each parameter can have different possible values and interactions. This increases the number of test cases required to cover the function's behavior. Additionally, debugging issues in functions with many parameters can be more challenging.
A function with many parameters might be tightly coupled to its callers. If one parameter changes, it might affect the function's behavior and need changes in many places.
So this case above was later just refactored to this:
function createUser(userData)
This is much better than before.
Also, in previous example, a function receives the data from one object. There is no need to pass them all one by one. You can pass the whole object:
// this is not OK
checkWindowForTrainingSchedule(
targetedTrainingSchedule.startDaysInterval,
targetedTrainingSchedule.endDaysInterval,
targetedTrainingSchedule.milestone,
targetedTrainingSchedule.name
);
// this is OK
checkWindowForTrainingSchedule(targetedTrainingSchedule);
In the end, it's a good practice to aim for functions with a reasonable number of parameters.
If you find that you need many parameters to achieve a certain functionality, consider using data structures like objects. Objects allow you to group related parameters together. You can also break down the function into smaller more focused functions. These functions can interact with each other to achieve the desired outcome.
This promotes cleaner, more maintainable, and more testable code.
Comments ()