Functions, Methods and Tricks [Final Part] By albro
In this post I want to cover the latest tips and tricks for writing clean functions. So let's start together!
DRY rule
There is a very important rule in programming known as the DRY rule, which stands for "don't repeat yourself". The important point of this rule is the reuse of codes, and based on it, we should not have the same code written several times in different parts of the program. If you have duplicate codes, it becomes very difficult to manage and maintain them. Imagine that you have repeated a certain logic for recording data in the database in 30 different parts of your code, but now the data structure has changed and this logic must also be edited. In this case, you have to edit your code 30 times, which is extremely annoying and increases the possibility of errors.
I have prepared one of the codes from the previous post with a little editing for you. In this code, you can see a simple example of non-compliance with DRY:function createUser(email, password) {
if (!inputIsValid(email, password)) {
showErrorMessage('Invalid input!');
return;
}
saveUser(email, password);
}
function createSupportChannel(email) {
if (!email || !email.includes('@')) {
console.log('Invalid email - could not create channel');
}
// ...
}
function inputIsValid(email, password) {
return email && email.includes("@") && password && password.trim() !== "";
}
function passwordIsValid(password) {
return password && password.trim() !== '';
}
function showErrorMessage(message) {
console.log(message);
}
function saveUser(email, password) {
const user = {
email: email,
password: password,
};
database.insert(user);
}
Notice the createSupportChannel
function from the code above. This function is supposed to create a support channel between us and our user so that we can communicate with each other (of course, the code inside is not real and is a simple log
). Before we want to create this support channel, we need to validate the user's email, so in the same way as the previous session, we have checked the presence of the @
sign inside it. Email and password validation is a function called inputIsValid
. Also, the operation of logging the message itself is repetitive because we had a function called showErrorMessage
that was responsible for printing the error message.
I explained to you in the previous post that one of the best ways to detect non-compliance with the DRY rule is that you copy your code from one section and paste it into another section. You should remember that this is not the only method of code detection. For example, in the code above, we have not copied and pasted, but we have repeated a certain operation (email validation), so DRY does not exclusively mean repeating words, but it also means unnecessarily repeating a simple logic.
To fix this, we can first use the showErrorMessage
function instead of console.log
. Also, the inputIsValid
function currently validates both email and password, but we only need email validation in the createSupportChannel
function. The best solution for this problem is to break the InputIsValid
function into two separate functions to check the email and password separately. Would it be a problem if we create a separate function called emailIsValid
and then use it inside the InputIsValid
function? For example:
function createUser(email, password) {
if (!inputIsValid(email, password)) {
showErrorMessage('Invalid input!');
return;
}
saveUser(email, password);
}
function createSupportChannel(email) {
if (!emailIsValid(email)) {
showErrorMessage('Invalid email - could not create channel');
}
// ...
}
function inputIsValid(email, password) {
return emailIsValid(email) && password && password.trim() !== '';
}
function emailIsValid(email) {
return email && email.includes('@');
}
function showErrorMessage(message) {
console.log(message);
}
function saveUser(email, password) {
const user = {
email: email,
password: password,
};
database.insert(user);
}
The problem with this code is that in the InputIsValid
function, we have different code levels; There is a function (emailIsValid
) on one side and we have manually checked the password on the other side. As I said, it is better to have a certain level of code in all our functions, so we can rewrite the above code as follows:
function createUser(email, password) {
if (!inputIsValid(email, password)) {
showErrorMessage('Invalid input!');
return;
}
saveUser(email, password);
}
function createSupportChannel(email) {
if (!emailIsValid(email)) {
showErrorMessage('Invalid email - could not create channel');
}
// ...
}
function inputIsValid(email, password) {
return emailIsValid(email) && passwordIsValid(password);
}
function emailIsValid(email) {
return email && email.includes('@');
}
function passwordIsValid(password) {
return password && password.trim() !== '';
}
function showErrorMessage(message) {
console.log(message);
}
function saveUser(email, password) {
const user = {
email: email,
password: password,
};
database.insert(user);
}
By doing this for the inputIsValid
function, we will again have two identical surfaces at once, and no further work is required.
Practical logic in DRY
In the previous section, we rewrote a code based on the DRY rule, but there is a very important point in dealing with DRY that should be addressed in this section. You should not follow these rules blindly and without real and practical logic. Why? Because you can apply these rules to any code, even readable code! Before breaking codes and editing them based on these rules, you should ask yourself if this really helps the code to be readable or not?
If you try to follow the mentioned rules without proper logic, you will face a new problem: excessive detail writing in the code! Excessive detailing in codes means that each part of your code is so small and divided into hundreds of parts that its readability is questioned. Writing small code is great and helps readability, but it shouldn't be overdone. I will give you three examples of situations where breaking codes into smaller parts is not recommended:
- You just want to change the name. For example, you have a function called
ValidateUserResponse
that validates the user's response, which is in the form of JSON. Now suppose you still need JSON validation in another part of the application, but this JSON is not the user's response, but it is related to another part of your server. If you can just useValidateUserResponse
without editing, there is no need to repeat it to rename it. - Cracked code is much harder to find than read. For example, if you have a function and its content is easy to read, there is no need to break it. In this case, if you break the code, you have to spend more time to find the broken codes and the code becomes more unreadable.
- You can't find a suitable name for it. If you feel that any chosen name for the broken code conflicts with the name of the main function or is too similar, it is probably better not to break the code. The problem here is that if you cannot choose a name for a function, it means that the function does not have a single and independent function and must be a part of other codes.
I have prepared an example for you that shows this very well. Note the saveUser
function from the code above:
// Other Codes
function passwordIsValid(password) {
return password && password.trim() !== '';
}
function showErrorMessage(message) {
console.log(message);
}
function saveUser(email, password) {
const user = {
email: email,
password: password,
};
database.insert(user);
}
It can be said that the saveUser
function has two separate levels of code; First, we have created a user
object in it, and then we have inserted that object into the database with a function called insert
, so we can transfer the user creation part to another function:
function createUser(email, password) {
if (!inputIsValid(email, password)) {
showErrorMessage('Invalid input!');
return;
}
saveUser(email, password);
}
function createSupportChannel(email) {
if (!emailIsValid(email)) {
showErrorMessage('Invalid email - could not create channel');
}
// ...
}
function inputIsValid(email, password) {
return emailIsValid(email) && passwordIsValid(password);
}
function emailIsValid(email) {
return email && email.includes('@');
}
function passwordIsValid(password) {
return password && password.trim() !== '';
}
function showErrorMessage(message) {
console.log(message);
}
function saveUser(email, password) {
const user = buildUser(email, password);
database.insert(user);
}
function buildUser(email, password) {
return {
email: email,
password: password,
};
}
In this case, the contents of saveUser
are on the same level and both are functions. In my opinion, splitting the saveUser
function into two functions is not a bad thing, but it is not ideal either. In my opinion, this division does nothing to make the code clean or readable, so breaking it up like this is pointless. If you remember, I explained three cases in which there was no need to break the code. In the code above, we have two of these three modes: First, we have only changed the name of the operation, that is, we have removed the const user
part and written buildUser
instead. In this operation, we had nothing but a simple name change. Secondly, our chosen name (buildUser
) is very similar to the name of the main function (createUser
) and it is a bit difficult to tell them apart for a new person in your team. If we want to break any code that was in the form above, at the end of the work we will have many unreadable codes because each function is divided into ten other functions and it will be very difficult to read them.
If for some reason you definitely want to break the saveUser
code so that you don't have different levels of code inside a function, you can use classes. Pay attention to the following example:
function createUser(email, password) {
if (!inputIsValid(email, password)) {
showErrorMessage('Invalid input!');
return;
}
saveUser(email, password);
}
function createSupportChannel(email) {
if (!emailIsValid(email)) {
showErrorMessage('Invalid email - could not create channel');
}
// ...
}
function inputIsValid(email, password) {
return emailIsValid(email) && passwordIsValid(password);
}
function emailIsValid(email) {
return email && email.includes('@');
}
function passwordIsValid(password) {
return password && password.trim() !== '';
}
function showErrorMessage(message) {
console.log(message);
}
class User {
constructor(email, password) {
this.email = email;
this.password = password;
}
save() {
database.insert(this);
}
}
function saveUser(email, password) {
const user = new User(email, password);
user.save();
}
function buildUser(email, password) {
return {
email: email,
password: password,
};
}
In this code, I have created a new class called User
, which takes the email and password and stores it in properties called email
and password
. Within this class, the save
method is responsible for saving the current instance of the class (a user) in the database. The saveUser
method takes the email and password and then creates a new instance of the class for the save
method to save. Finally, we have the buildUser
method, which is responsible for building the user
object (user information). Using this method is much better than the previous method and if you insist on breaking saveUser
I suggest this method. Although in this method, it is better to put the rest of the code (such as validation) inside the class so that the programming becomes completely object oriented.
Function effects (Pure functions)
Another rule of writing clean code is that you should try to keep your functions as pure as possible. Pure functions are functions that produce a specific output for a specific input, or in other words, each specific input always has a specific output. The following function is a simple example of a pure function:
function generateId(userName) {
const id = 'id_' + userName;
return id;
}
This function appends the _id
string to the beginning of userName
and passes it to us, so whatever value we give it, we get exactly the same plus the _id
string. For example, if I pass the string Albro
to this function a thousand times, I will receive the id_Albro
value a thousand times. We call this class of functions pure functions.
On the other hand, if we want to give an example for non-pure functions, the following function will be a good example:
function generateId(userName) {
const id = userName + Math.random().toString();
return id;
}
Non-pure functions are functions that produce a different response for a given input. For example, in the above function, we create a random number each time using the Math.random
function and attach it to the username
. The existence of non-pure functions in the code is not a bad thing, but it is unavoidable, and in any program, we will eventually need such codes. The problem here is that non-pure functions are less predictable, so it's preferable to switch to non-pure functions only when needed and write your functions as pure as possible.
The very important point here is that pure functions do not have any side effects. To understand what a side effect is, you should pay attention to the following example:
function createUser(email, password) {
const user = new User(email, password);
return user;
}
The above function creates an instance of the User
class and then returns this created user. This function is a pure function and has no effects because every time we call it, a specific user (with a specific email and password) will be created. Now suppose the above function is written like this:
function createUser(email, password) {
const user = new User(email, password);
startSession(user);
return user;
}
The startSession
function creates a new session for our user. Naturally, creating a new session changes the general state of our program (for example, a user who has a session can access certain parts of the program). Function effects are operations that have nothing to do with the input and output of the function, but change the general state or state of the program. For example, in the code above, startSession
will change a variable outside the createUser
function, so we can see that it is not limited to this function and it will change the state of our program.
If your function always returns a constant output for a specific input but has effects, it is no longer considered a pure function. There are two conditions for functions to be pure: first, to have a specific and constant output for a specific input. Second, our function should not have any effects. Keep in mind that the presence of effects is not bad in itself, but unavoidable. For example, displaying text to the user (such as logging) or sending HTTP requests or creating a session are all considered effects because they change the general state of the program. Your job as a developer is to avoid unpredictable effects. For example, the function I gave you as an example has effects, but its effects are predictable:
function createUser(email, password) {
const user = new User(email, password);
startSession(user);
return user;
}
We know how to create a user and create a session for it, and it is logically correct, while the effect in the following function is unpredictable:
let lastAssignedId;
function generateId(userName) {
const id = 'id_' + userName;
lastAssignedId = id;
return id;
}
This function is very similar to our previous function, which was responsible for generating id, but the difference is that we have given the generated id to a global variable named lastAssignedId
! You might say that whatever code we write is written by us, so it is predictable, but I don't mean that the effects are predictable! If someone wants to call a function called generateId
, they don't expect that such a function will store the generated ID in a global variable and do something with it later. By predictability, we mean predictability by people who are just reading your code. Also remember that the code above works and is not a problem in terms of execution, but we are talking about its cleanliness.
A simple solution is to change the name of the function to generateUnsedId
or generateAndTrackId
so that anyone who sees it will know that this function is tracking the ID in addition to generating it. Whatever name you choose for your functions should reflect the side effects of that function or method, not confuse other developers. Notice some function names below:
saveUser
: seeing this name, we know that the function probably has an effect because saving data in the database is considered an effect. Why? Because it changes the state of the program and is not limited to this function. The existence of effects is clear from the name of this function, so there is no problem.- isValid: The name of this function does not cause any doubt about the existence of effects because it is responsible for validation, so it probably takes data and returns the result of its validation and does not need to change the state of the program. Naturally, you can write this function in such a way that, for example, it sends a message to the user, and in this case we will have an effect, but if you have such a function, you should change its name to reflect this effect.
- showMessage: The meaning of the name of this function is "Show the message", so just by reading its name, we realize that this function has effects. Displaying the message whether it is in the terminal or in the console or even directly on our website is still considered an effect. Why? Because the state of the program changes in this mode. The state of the program without a message will be different from the state of the program after that message.
- createUser: This name does not specify the condition of the effect at all, and we can have both interpretations of it. On the one hand, createUser can only create the user object, but on the other hand, we may mean the complete process of creating a user and storing it in the database.
Therefore, we should pay attention to two important points regarding the effects of functions: First, if your function has an effect, be sure to choose a name that explains the existence of this effect. Second, if you don't want to change the name of the function, you should transfer the effect to another function.
Exercise: Solving effects
I have prepared a code for you that has problems in the field of function effects. I ask you to correct it:
function connectDatabase() {
const didConnect = database.connect();
if (didConnect) {
return true;
} else {
console.log('Could not connect to database!');
return false;
}
}
function determineSupportAgent(ticket) {
if (ticket.requestType === 'unknown') {
return findStandardAgent();
}
return findAgentByRequestType(ticket.requestType);
}
function isValid(email, password) {
if (!email.includes('@') || password.length < 7) {
console.log('Invalid input!');
return false;
}
return true;
}
To solve this code, we start from the first function, connectDatabase
. When the function name is connectDatabase
(connect to the database), we can easily understand that its job is to connect to the database, so it is considered an effect. If you look at the code inside this function, you will see two different effects. The first effect is connecting to the database (database.connect) but the second effect is the console.log command because it prints a message to the console.
We need to decide here if this effect belongs in connectDatabase
or if it should be changed. On the other hand, it can be claimed that if the functional task is to connect to the database, it is likely that error management will also happen there, so this code has no problem. On the other hand, it can be argued that the name of the function only specifies the connection to the database and does not say anything about error management, so the person who calls this function (without seeing its content) cannot notice that the message is sent from this side. be subject I'm just explaining the different modes of this and I can't give you a definitive answer because the definitive answer depends on the structure of your program as well as your taste.
If you want to have the second interpretation of this function, you should transfer the message print part to another function:
function initApp() {
try {
connectDatabase();
} catch (error) {
console.log(error.message);
// showErrorMessage(...)
}
}
function connectDatabase() {
const didConnect = database.connect();
if (!didConnect) {
throw new Error('Could not connect!');
}
}
In the next step, we reach the second function called determineSupportAgent
. This function finds a support person for our support ticket to contact the user. With a simple look at this code, we realize that there is probably no effect. The third method is the isValid
method, which has an effect and this effect is also unpredictable. If the function name is isValid
we expect to get true
or false
but this function also has console.log
. The easiest way is to not show a message at all in this section so that all the codes of this challenge look like this:
function initApp() {
try {
connectDatabase();
} catch (error) {
console.log(error.message);
// showErrorMessage(...)
}
}
function connectDatabase() {
const didConnect = database.connect();
if (!didConnect) {
throw new Error('Could not connect!');
}
}
function determineSupportAgent(ticket) {
if (ticket.requestType === 'unknown') {
return findStandardAgent();
}
return findAgentByRequestType(ticket.requestType);
}
function isValid(email, password) {
return email.includes('@') && password.length >= 7;
}
But if you definitely want a message to be displayed, it is better to do that operation in your parent function. For example, in a function called createUser
, check the email and password and if it is not valid, display the error message to the user.
[hive: @albro]
Congratulations!
✅ Good job. Your post has been appreciated and has received support from CHESS BROTHERS ♔ 💪
♟ We invite you to use our hashtag #chessbrothers and learn more about us.
♟♟ You can also reach us on our Discord server and promote your posts there.
♟♟♟ Consider joining our curation trail so we work as a team and you get rewards automatically.
♞♟ Check out our @chessbrotherspro account to learn about the curation process carried out daily by our team.
🥇 If you want to earn profits with your HP delegation and support our project, we invite you to join the Master Investor plan. Here you can learn how to do it.
Kindly
The CHESS BROTHERS team
https://inleo.io/threads/albro/re-leothreads-wn54cdhm
The rewards earned on this comment will go directly to the people ( albro ) sharing the post on LeoThreads,LikeTu,dBuzz.
Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!
Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).
You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support.
This is a hive-archeology proxy comment meant as a proxy for upvoting good content that is past it's initial pay-out window.
Pay-out for this comment is configured as followed: