DEV Community

Cover image for How I Write Clean Code As A Develeoper : Step By Step
Durgesh kumar prajapati
Durgesh kumar prajapati

Posted on

How I Write Clean Code As A Develeoper : Step By Step

These are some steps for write a clean code, I have also included my perspectives and preferences.


Naming

  • Use descriptive name I believe that this point is emphasized the most: “Names in software are 90 percent of what make software readable.” Let’s consider the following code examples. The variable p reveals nothing. Despite having a comment explaining its purpose, it can reduce the readability of overall codes. In Example 2, those variables clearly describe their intended functions.
// Example 1
var p float64 // final price

// Example 2
var basePrice float64
var totalDiscount float64
var finalPrice float64
Enter fullscreen mode Exit fullscreen mode
  • Avoid disinformation Avoid ambiguous names; be clear about what the variable, class, or function truly does. Don’t overuse abbreviation, such as r for radius and w for width, except they are commonly used such as ctx for context in Go.
  • Use pronounceable and searchable names Coding may seem like an individual task, but in a collaborative team, it becomes a social activity. Use names that are easy to pronounce to facilitate communication.

Functions

  • Single Responsibility Principle A function should have a single, clearly defined responsibility indicated by its name. For instance, getUserData() should exclusively return user data without performing other tasks. Don’t hide side effects within the name of a function. Take a look at this example. This function should only update transaction status, but it also sends email to user. Furthermore, the returned result originates from SendEmailToUser(), which it is supposed to be UpdateTransactionData() . This example violates Single Responsibility Principle.
func (s *service) updateTransactionStatus(ctx context.Context, 
  transaction UpdateTransactionRequest) (
  result UpdateTransactionResponse, err error) {
  result, err := s.UpdateTransactionData(ctx, transaction)
  if err != nil {
    return result, err
  }

  result, err := s.SendEmailToUser(ctx, transaction)
  if err != nil {
    return result, err
  }

  return result, err
}
Enter fullscreen mode Exit fullscreen mode
  • As small as possible
    A small function simplifies the creation of unit tests and facilitates the early detection of potential bugs. This principle is closely connected to the Single Responsibility Principle.

  • Number of arguments
    It’s advisable to limit the number of arguments for a function. The most recommended are niladic functions (with no arguments), followed by monadic functions (with one argument) and dyadic functions (with two arguments). It’s best to avoid triadic functions (with three arguments) whenever possible. Fewer arguments simplify testing and reduce the chances of input errors due to argument swapping.
    In Go, it’s advisable to include the context as an argument. We can manage function complexity and limit the number of arguments to a maximum of two whenever possible. This recommendation also applies to multiple return values in Go. While returning two values like return result, err is acceptable, for three or more values, it’s preferable to wrap them in a single struct. Regardless, always choose descriptive names that clearly convey the intention of each return value.

// not recommended
func createTransaction(ctx context.Context, 
  userID int,
  transactionType string,
  description string,
  amount float64) (err error, status string, endingAmount float64) {
  // ...
}

// recommended
func createTransaction(ctx context.Context, 
  transactionData CreateTransactionRequest) 
  (err error, result CreateTransactionResponse) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode
  • Expected arguments Always manage to use the expected arguments. Consider this example. The second example may seem exaggerated, but we sometimes unconsciously overuse arguments.
// expected argument
func isValidUser(userID int) bool {
  // process
}

// unexpected arguments
func isValidUser(userID int, numberOfFriend int) (isValid bool, friendIDs []int) {
  // process
}
Enter fullscreen mode Exit fullscreen mode

Comments

  • Unused and dead code Delete unused functions unless there is a clear comment explaining why it’s beneficial to keep them.
  • Redundant comment “Comment should say things that the code cannot say for itself.” See the following example of redundant comment.
i += 1 // increment of i
Enter fullscreen mode Exit fullscreen mode
  • Explanation of intent I think it’s acceptable to use comments to provide an outline of the flow or important information for others to understand the code. However, it’s crucial to avoid unnecessary comments. According to Uncle Bob, comment can be used to provide the intent behind a decision. See the examples below. Additionally, comments can serve to alert others about the potential consequences of running specific functions.
// ===== NOT RECOMMENDED ======
// iterate over the list of user ID
for _, id := range userIDs {
  // do something with id
}

// the day after tomorrow
var dayAfterTomorrow string

// ===== RECOMMENDED =====
// sometimes the clients don't get the callback, so we need to resend it
result, err := resendCallback(ctx, request)
Enter fullscreen mode Exit fullscreen mode
  • Journal comments
    Rather than including all changes, logs, and contributors at the start of the code, Uncle Bob prefers to omit this information, relying on version control systems for tracking these details.

  • Out-of-update comment
    Sometimes, the next developer may not be aware of existing comments, and when they make changes, these comments often go unmodified. It’s important to be mindful of comments that require regular updates. If the naming or logic of the code is clear enough on its own, consider deleting these comments.


General

  • Follow coding standard conventions If the team follows industry conventions, adhere to them.
  • Replace magic numbers with constants However, there are cases where using magic numbers is acceptable, especially when calculations involve well-known measurements. For less common numbers, constants are a better choice.
// using magic numbers vs consts; magic number here is more preferable
totalSeconds = days * 24 * 60 * 60
totalSeconds = days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE

// using magic numbers vs consts; const is more preferable
remainingTimeOff = 20 - takenTimeOff
remainingTimeOff = MAX_TIME_OFF - takenTimeOff
Enter fullscreen mode Exit fullscreen mode
  • Vertical separation In Go, it’s common practice to define variables and functions close to where they are used. For instance, the declaration of latestTransaction is placed before its assignment, rather than declaring it at the very beginning of the program.
transactionList, err := repository.GetTransactionList(ctx, ID)
if err != nil {
  return response, err
}

var latestTransaction Transaction
latestTransaction = transactionList[len(transactionList)-1]

Enter fullscreen mode Exit fullscreen mode
  • Be consistent Once you’ve selected a convention, maintain consistency throughout your codebase to prevent confusion. If you opt for the term Userstick with it consistently and avoid introducing Customerin the middle of your code unless they represent distinct concepts.
  • Selector arguments Instead passing true or false as argument to select the behavior, it’s preferable to create separate functions to handle each behavior.
  • Use explanatory variables In many cases, splitting code into multiple lines can improve readability, but it should be done thoughtfully. The choice between one-liners and multiline code depends on the specific context and what makes the code more understandable. The following example maybe too simple to be broken down into multiple lines, but the idea is to clarify the logic so the other will get it easily.
// one line
price := basePrice - (basePrice * discount) + (basePrice * tax)

// multiple lines
priceAfterDiscount := basePrice - (price * discount)
finalPrice := priceAfterDiscount - (price * tax)
Enter fullscreen mode Exit fullscreen mode

Connect with me

LinkedIn : https://www.linkedin.com/in/durgesh4993/
GitHub : https://github.com/Durgesh4993
LeetCode : https://leetcode.com/durgesh4993/
Profile : https://linktr.ee/durgesh4993

Keep Supporting :)

Top comments (7)

Collapse
 
joro550 profile image
Mark Davies • Edited

I couldn't read past the functions example as this is very skin deep - you present an example where a function is doing two things - but those two things are the business logic - you suggest pulling them into smaller more contained functions which is fine but never solve the business need?

To be explicit in your first example you present code that does two things - updates a transaction and then sends an email and you say that this breaks the 'single responsibility pattern' (I heavily disagree) and you say that those should be two functions instead. But the business case here is that you want to update the transaction and then send an email - but present no way of actually solving that particular problem you just say 'single responsibility' and move on - how am I supposed to do two those two things? if I have a separate class that calls those two new methods - is that now breaking your precious single responsibility?

Collapse
 
roenfeldt profile image
Daniel

When it comes to adhering to the single responsibility principle, my understanding is, that it all comes down to identifying the main objective of the business logic we’re trying to achieve, splitting it up into smaller parts that are supposed to take care of only one action, and writing a function for each, making sure it achieves the action and nothing else. Just like a list of tasks with subtasks. For instance, the main objective could be going on a roadTrip(). We need our car to be in perfect condition, fueled up, and clean. We ask the mechanic to checkTheCar() (the mechanic doesn’t care how much fuel there is in the tank, nor he will be asked to clean up the car). As soon as the mechanic gives us the thumbs up, certifying that the car is in perfect working condition, we then go to the petrol station and fuelUpTheCar(). Once that’s done, all that remains is to make sure the car is clean. We head over to the car wash and cleanTheCar(). Everything’s ready, all that remains is that we pack up our luggage and go on our adventurous roadTrip().

Collapse
 
joro550 profile image
Mark Davies

In the post in the SRP section it says :

Single Responsibility Principle A function should have a single, clearly defined responsibility indicated by its name. For instance, getUserData() should exclusively return user data without performing other tasks.

putting aside that we're defining responsibility with the word responsibility - we're being given an example of the 'glue code' if a function can only ever do one thing then where do I put my logic?

func roadTrip() {
mechanic.check(car)
station.fuel(car)
}
Enter fullscreen mode Exit fullscreen mode

Does this break the principle? I couldn't tell you because this blog post just tells me that functions should doe only one thing do I guess I can't do this because this does two things?

Thread Thread
 
brense profile image
Rense Bakker

Func roadtrip doesnt violate SRP because its task is to make a road trip. It does this by executing sub tasks which also have a single responsibility. You'd violate single responsibility if the mechanic.check(car) would also fuel the car.

Collapse
 
get_pieces profile image
Pieces 🌟

This is a great article. 🤩

Collapse
 
emmabase profile image
Emmanuel Eneche

Thanks for sharing. Nice article 👏

Collapse
 
shedrick profile image
Shedrick Williams

I just got done refactoring a small project that I was completely unable to comprehend due to poor naming (took hours and hours). So I can definitely see the importance of your first tip 😭. Thank you